在我們今天的旅程中,我們一起探索TypeScript中那些令人興奮的泛型知識。從類型推斷的便捷性到泛型在日常編程中的靈活運用,希望這些內容能夠幫助你解開圍繞泛型的所有迷霧。記住,泛型不僅僅是類型安全的保障,它還能讓你的代碼更加簡潔、更易于維護。
在編程世界里,我們經常會遇到一個情況:閱讀那些充滿了虛構示例的枯燥文檔,實在是讓人提不起興趣。因此,在這篇文章中,我想和大家分享一些我在實際開發過程中遇到的泛型(Generics)使用案例。通過這些真實的例子,相信泛型的概念對你來說會更加具有意義,也更容易理解。
泛型簡介
那么,泛型究竟是什么呢?簡而言之,泛型允許我們編寫能夠適用于廣泛的原始類型和對象的類型安全代碼。在聲明新類型、接口、函數和類時,都可以使用泛型。這聽起來可能有點抽象,那么讓我們直接進入正題,看看泛型的一些實際用例吧。
代碼重復
有時候,在我們開發的時候會遇到一些重復性的工作,特別是當我們要處理不同類型的數據時。這里有個很好的例子,就是我們的服務器需要返回用戶和書籍信息。通常情況下,如果沒有泛型(Generics),我們可能需要為每種資源分別定義一個響應類型。
舉個例子,你的服務器需要返回用戶信息和書籍信息。
如果沒有泛型,你可能需要為用戶和書籍分別定義兩個相似的響應類型,就像下面這樣:
// 用戶信息類型
type User = { name: string };
type UsersResponse = {
data: User[];
total: number;
page: number;
limit: number;
};
// 書籍信息類型
type Book = { isbn: string };
type BooksResponse = {
data: Book[];
total: number;
page: number;
limit: number;
};
但這種方式會產生很多重復的代碼,不利于后期維護。而泛型,它的妙處就在于可以讓我們定義一個通用的響應形狀,然后再根據需要使用不同的數據類型來復用這個形狀,這樣就能減少重復的代碼,看看下面這個改進版:
// 分頁響應的泛型定義
type PaginatedResponse<T> = {
data: T[];
total: number;
page: number;
limit: number;
};
// 使用泛型定義用戶和書籍的響應類型
type UsersResponse = PaginatedResponse<User>;
type BooksResponse = PaginatedResponse<Book>;
使用了泛型之后,無論是處理用戶列表還是書籍列表,我們只需要寫一次響應結構,就可以應用到各種不同的數據類型上了,不是很方便嗎?
泛型就像是一個萬能的模具,你只需要根據不同的需求,換上不同的"原料",它就能幫你塑形出符合要求的"產品"。這樣我們的代碼就會變得更簡潔、更有可讀性,也更容易維護。
現在來想想,你是否能在你的項目中找到那些可以用泛型來簡化的地方呢?別小看這個小改變,它可能會為你省下不少時間和精力哦!
泛型,讓函數的邏輯和類型更匹配
在軟件開發中,我們常常需要編寫一些根據特定屬性篩選數組元素的函數。比如我們有一個篩選數組的函數 filterArrayByValue,它可以基于我們提供的屬性和值來過濾數組。函數的參數和返回值之間的關系非常緊密。
一開始,我們的函數可能看起來是這樣的:
function filterArrayByValue(items, propertyName, valueToFilter) {
return items.filter((item) => item[propertyName] === valueToFilter);
}
這個函數聲明說,它接受一個項目數組,并返回一個具有相同類型項目的數組。目前為止,一切都好。
但是這里有個問題,我們的 propertyName 參數被定義為字符串類型,這看似沒問題,但它可能會導致我們不小心傳入了不存在于類型 T 的項的屬性名。如果我們定義了一個用戶數組,它應該是這樣的:
type User = { name: string; age: number };
const users: User[] = [
{ name: 'Vasya', age: 32 },
{ name: 'Anna', age: 12 },
];
現在,如果我們嘗試傳遞一個錯誤的屬性,在這種情況下它不會破壞應用程序,只是返回一個空數組,但是這并不是我們希望的,我們希望編譯器會提示屬性不匹配的問題。
filterArrayByValue(users, 'notExistField', 'Vasya');
讓我們定義該函數的第二個參數,它將描述限定為只能為T類型的相關的屬性
我們定義完后,發現在運行階段之前提示傳遞了錯誤的屬性。
接下來我們使用 number 類型的age 屬性。正如您可能預測的那樣,當我們嘗試按此字段過濾項目時,我們會遇到問題:
filterArrayByValue(users, 'age', 12);
接下來我們修改過濾函數,valueToFilter參數的對應關系,匹配為T類型屬性對應的值
修改后,問題已經消失了,現在我們無法將除了數字以外的其他類型的值作為年齡屬性值傳遞,因為用戶類型只允許該屬性為數字,這正是我們需要的。
在 React 中的應用
在React開發中,狀態管理是一個核心概念,尤其是在使用函數組件和Hooks的時候。給出的代碼段展示了如何在React組件中使用 useState Hook來管理一個用戶對象的狀態,并提供了一個 setUserField 函數來更新用戶對象的特定字段。原始版本的函數對于字段名和字段值使用了非常寬泛的類型定義,這可能會導致類型安全問題。
const setUserField = (field: string, value: any) =>
setUser(prevUser => ({...prevUser, [field]: value}));
這里,field 是任意的字符串,而 value 是任意類型,這意味著我們可以不小心將錯誤的數據類型賦值給用戶對象的屬性,TypeScript編譯器也不會提出警告。
為了提高類型安全性,可以使用泛型來約束 field 必須是 User 類型的鍵,value 必須是對應于該鍵的 User 類型的值。改進后的 setUserField 函數如下:
function setUserField<KEY extends keyof User>(
field: KEY,
value: User[KEY]
) {
setUser((prevUser) => ({ ...prevUser, [field]: value }));
}
在這個改進的版本中,setUserField 函數現在接受兩個參數:
- field:一個類型參數 KEY,它被限制為 User 類型的鍵的集合中的一個。
- value:一個 User[KEY] 類型的值,確保了傳遞給 setUserField 的值必須與 User 類型中 field 字段的類型相匹配。
這樣一來,如果你嘗試傳遞一個不正確的字段或者錯誤類型的值給 setUserField 函數,TypeScript編譯器會提供類型錯誤的提示,從而減少運行時錯誤的可能性。這種模式特別有用,因為它可以保證我們對狀態的更新是類型安全的,同時也保持了函數的靈活性。這是React中使用TypeScript的一個典型例子,展示了如何通過類型系統來增強代碼質量。
同時保持靈活和嚴格(關鍵詞“擴展extend”與泛型)
當我們在設計高階組件(HOC)時,尤其是在React或React Native的環境下,我們希望這些HOC只能應用于具有某些屬性的組件。在這個例子中,我們想要一個HOC,它僅適用于具有 style 屬性的組件。
function withStyledComponent<StyleProp, Props extends { style?: StyleProp }>(
Component: ComponentType<Props>
) {
return (props: Props) => {
const { style } = props;
// 實現細節在此省略
return <Component {...props} />;
};
}
泛型的 extend 關鍵字允許我們定義一個類型 T,它必須至少具有類型 K 的所有屬性。這樣,我們就可以確保我們的HOC只會被用在正確的組件上。
在上述的 withStyledComponent HOC中,我們指定了任何使用此HOC的組件都必須有一個 style 屬性。如果我們嘗試將這個HOC應用于沒有 style 屬性的組件,TypeScript會拋出一個錯誤。
這種模式非常有用,因為它可以保證我們的HOC在類型安全的同時,也不限制組件的其他屬性。這就意味著,盡管我們對 style 屬性有明確的期望,但我們的組件可以自由地具有其他任何屬性。
此外,由于TypeScript知道我們可能會在具有 style 屬性的組件中使用我們的HOC,我們可以安全地從組件的屬性中提取 style 并在HOC內部操作它。
TypeScript中的類型推斷
TypeScript有一個令人驚嘆的特性——它會嘗試從上下文中推斷出類型,只要有可能。比如,在代碼中看到這樣的語句時:
const a: number = 12;
這意味著開發者可能并不知道TypeScript已經知道a是一個從值推斷出來的數字類型。
現在,假設我們用泛型定義了這樣一個函數:
function identifyType<T>(target: T) {
console.log("Type of target is", typeof target);
}
如果你是初學者,你可能會這樣使用它:
identifyType<number>(5);
但是,TypeScript可以從你作為第一個參數傳遞的值中推斷出泛型的類型,最好是這樣使用:
identifyType(5);
如果你是React開發者,你可能會經常看到像這樣的代碼片段:
const [count, setCount] = useState<number>(5);
但同樣,這里明確定義泛型類型是多余的,因為它會從你作為第一個參數傳遞的值中被推斷出來。如果你是一位經驗豐富的開發者,你的代碼將看起來像這樣:
const [count, setCount] = useState(5);
還有我遇到過的一個情況,有開發者害怕在React組件的props中使用泛型。是的,我們在JSX中使用我們的組件,他們不知道這樣的語法是有效的:
function Component() {
const data: ItemType[] = [{ value: '1' }];
return (
<RenderList<ItemType>
data={data}
renderItem={({ item }) => <Text>{item.value}</Text>}
/>
);
}
是的,它看起來有些奇怪,但這里我們可以依靠TypeScript的能力,根據我們傳遞給組件的props類型來推斷泛型類型:
<RenderList
data={data}
renderItem={({ item }) => <Text>{item.value}</Text>}
/>
認同這樣的代碼看起來更簡潔,你看起來也像一個經驗豐富的開發者。這就是TypeScript和泛型的魅力:它們提供了一種強大的類型系統,不僅可以幫助我們減少錯誤,還可以使代碼更加簡潔易讀。通過這些例子,我們可以看到,TypeScript的類型推斷功能可以在不犧牲類型安全的情況下,極大地簡化代碼。而泛型的靈活使用,則讓我們的代碼既嚴謹又富有彈性。
結束
在我們今天的旅程中,我們一起探索了TypeScript中那些令人興奮的泛型知識。從類型推斷的便捷性到泛型在日常編程中的靈活運用,希望這些內容能夠幫助你解開圍繞泛型的所有迷霧。記住,泛型不僅僅是類型安全的保障,它還能讓你的代碼更加簡潔、更易于維護。
正如我們所見,合理利用TypeScript的類型推斷,可以讓我們避免冗余的代碼,讓邏輯表達更為直觀。泛型的使用更是讓組件和函數的復用性達到了新的高度。所以,當你下次遇到需要類型化處理多樣化數據的場景時,別忘了,泛型就是你的得力助手。