generics


Posted by TempuraEngineer on 2022-12-31

目錄


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


#TypeScript #generic #泛型







Related Posts

React-[useEffect篇]- 如何選擇一個項目並且切換畫面

React-[useEffect篇]- 如何選擇一個項目並且切換畫面

[ MTR04 ]  程式基礎(下)

[ MTR04 ] 程式基礎(下)

[Note] Git: 常用指令

[Note] Git: 常用指令


Comments