目錄
generics 是什麼
generics 就是泛型,所謂的泛型是指可以用傳引數(argument)的方式來指定型別
,這麼做能提升型別定義的彈性
,進而增加複用性
可以把使用泛型想像成製作組件的時候挖 slot 或者 props 的感覺
基本
假設要回傳收到的參數內 index 為 0 的元素,你的 code 可能會長這樣
function foo(arg: number[]): number {
return arg[0];
}
function bar(arg: string[]): string {
return arg[0];
}
那如果參數型別是 boolean[],不就要再多加一個函式 🤔
沒錯,所以才要用泛型,因為泛型可以給予型別定義適當的彈性
它既不會像上一段的 code 的型別定義那樣缺乏彈性,也不會像 any、unknown 一樣過度彈性
function foo<Type>(arg: Type[]): Type {
return arg[0];
}
// 也可以不寫<number>、<boolean>,Typescript會自動判斷型別
const getNumber = foo < number > [1, 2, 3];
const getBoolean = foo < boolean > [true, true, false];
泛型函式(generic function)
泛型函式的型別定義方式有 2 種,分別為用<>或{}包起來
function foo<Type>(arg: Type[]): Type[] {
return arg;
}
用<>包起來
const foo1: <Input>(value: Input[]) => Input[] = (value) => value;
// 用{}包起來
const foo3: { <Input>(value: Input[]): Input[] } = (value) => value;
多個型別參數
泛型函式也可以接收多個引數,只要在<>內多寫一個參數即可
type Profile = {
name:string;
job:'ninja' | 'samurai',
favor:string
}
function foo<T, K >(profile:T, detail:K extends T){
return {
profile,
detail
}
}
// 使用Pick可以挑出型別中需要的部分
const tempuraSamurai = foo<Pick<Profile, 'name' | 'favor'>, {weapon:string}>({name:'TempuraSamurai', favor:'shrimp'}, {name:'TempuraSamurai', favaor:'shrimp', weapon:'katana made with hijiki'});
// 也可以不傳入引數,TS會自行判斷
const tempuraNinja = foo({name:'TempuraNinja', job:'ninja'}, {name:'TempuraNinja', job:'ninja', skill:['eating onigiri', 'writing words']});
enum Weekday {
'Monday' = 'Monday',
'Tuesday' = 'Tuesday',
'Wednesday' = 'Wednesday',
'Thursday' = 'Thursday',
'Friday' = 'Friday',
}
enum Weekend {
'Sunday' = 'Sunday',
'Saturday' = 'Saturday',
}
function bar<T, K>(val: keyof T extends keyof K){
return val;
}
const res2 = bar<typeof Weekday, typeof Weekend>('Sunday');
傳遞型別參數給內部函式
// 函式表達式 + <>
const foo = <T, K extends keyof T>(prop: K, value: T[K]) => {
return (item: T): boolean => {
return item[prop] === value;
}
}
比較不推薦用{},因為在這種情況下會變得難以閱讀
// 函式表達式 + {}
const bar:{<T, K extends keyof T>(props:K, value:T[K]):(item:T) => boolean} = (prop, value) => {
return (item) => {
return item[prop] === value;
}
}
泛型與限制
泛型是有些限制的,並不是傳入一個東西,隨便要求回傳一個沒有的屬性也行
例如這個例子是回傳傳入參數的 length 屬性,但傳入的可能是 number、boolean 等沒有 length 的值,所以會報錯"Property 'length' does not exist on type 'Type'."
function foo<T>(arr: T) {
return arr.length;
}
這時需要定義一個 interface 或者 type 來將這個型別參數更加具體化
interface withLength {
length:number;
}
// 限定傳入的參數一定要有length屬性
function foo<T>(arr:T extends withLength){
return arr.length;
}
實際例子
PropsWithChildren
React 的 PropsWithChildren 就是泛型的一個例子
type PropsWithChildren<P> = P & { children?: ReactNode | undefined };
假設有幾個 props 它們都有 children 這個屬性,而其他屬性則不相同
只要把不同的地方抽掉改成型別參數
,就能得到一個有 children 屬性的物件型別
interface MapProps {
center: number[];
zoom: number;
}
interface TooltipProps {
position: 'top' | 'bottom';
permanent: boolean;
}
// 這樣就不用在每個type都寫children了
const Tooltip = ({position, permanent, children}:PropsWithChildren<TooltipProps>) => (// 省略... )
const BikeStationMap = ({center, zoom, children}:PropsWithChildren<MapProps>) => (// 省略... );
HOC
假設有幾個 component 都要套同的 Layout
如果用 HOC 的話,因為我們無法預知以後會用到 Layout 的 componet 有哪些 props,所以 WrappedComponent 的型別就會用到泛型
// WrappedComponent不定義為FunctionComponent<T>是因為props必是一個key為string的物件,所以用Record更加具體化型別會比較好
const withLayout: {
<T>(
WrappedComponent: FunctionComponent<Record<string, T>>,
): (WrappedComponent: FunctionComponent<Record<string, T>>) => JSX.Element,
} = (WrappedComponent) => {
const ComponentWithLayout = (props: Record<string, T>) => (
<Layout>
<WrappedComponent {...props} />
</Layout>
);
return ComponentWithLayout;
};
參考資料
Typescript - Generics
How to apply generic type for inner function in typescript