React + TypeScript 實現泛型組件

  • 2019 年 10 月 3 日
  • 筆記

泛型類型

TypeScript 中,類型(interface, type)是可以聲明成泛型的,這很常見。

interface Props<T> {    content: T;  }

這表明 Props 接口定義了這麼一種類型:

  • 它是包含一個 content 字段的對象
  • content 字段的類型由使用時的泛型 T 決定
type StringProps = Props<string>;    let props: StringProps;    props = {    // ? Type 'number' is not assignable to type 'string'.ts(2322)    content: 42  };    props = {    //    content: "hello"  };

或者,TypeScript 能夠跟使用時候提供的值自動推斷出類型 T,無需顯式指定:

interface Props<T> {    content: T;  }    function Foo<T>(props: Props<T>) {    console.log(props);  }    /** 此時 Foo 的完整簽名為: function Foo<number>(props: Props<number>): void */  Foo({ content: 42 });    /** 此時 Foo 的完整簽名為: function Foo<string>(props: Props<string>): void */  Foo({ content: "hello" });

上面因為 Foo 函數接收 Props<T> 作為入參,意味着我們在調用 Foo 的時候需要傳遞類型 T 以確定 Props<T>,所以 Foo 函數也變成了泛型。

當調用 Foo({ content: 42 }) 的時候,TypeScript 自動解析出 Tnumber,此時對應的函數簽名為:

function Foo<number>(props: Props<number>): void;

而我們並沒有顯式地指定其中的類型 T,像這樣 Foo<number>({ content: 42 });

泛型組件

將上面的 Foo 函數返回 JSX 元素,就成了一個 React 組件。因為它是泛型函數,它所形成的組件也就成了 泛型組件/Generic Components

function Foo<T>(props: Props<T>) {    return <div> {props.content}</div>;  }    const App = () => {    return (      <div className="App">        <Foo content={42}></Foo>        <Foo<string> content={"hello"}></Foo>      </div>    );  };

一如上面的討論,因為 TypeScript 可根據傳入的實際值解析泛型類型,所以 <Foo<string> content={"hello"}></Foo>string 是可選的,這裡只為展示,讓你看到其實 React 組件還可以這麼玩。

為了進一步理解泛型組件,再看下非泛型情況下上面的組件是長怎樣的。

interface Props {    content: string;  }    function Foo(props: Props) {    return <div>{props.content}</div>;  }    const App = () => {    return (      <div className="App">        {/* ? Type 'number' is not assignable to type 'string'.ts(2322) */}        <Foo content={42}></Foo>        <Foo content={"hello"}></Foo>      </div>    );  };

以上,便是一個 React 組件常規的寫法。它定義的入參 Props 只接收 string 類型。由此也看出泛型的優勢,即大部分代碼可復用的情況下,將參數變成泛型後,不同類型的入參可復用同一組件,不用為新類型新寫一個組件。

除了函數組件,對於類類型的組件來說,也是一樣可泛型化的。

interface Props<T> {    content: T;  }    class Bar<T> extends React.Component<Props<T>> {    render() {      return <div>{this.props.content}</div>;    }  }    const App = () => {    return (      <div className="App">        <Bar content={42}></Bar>        <Bar<string> content={"hello"}></Bar>      </div>    );  };

一個更加真實的示例

一個更加實用的示例是列表組件。列表中的分頁加載,滾動刷新邏輯等,對於所有列表數據都是通用的,將這個列表組件書寫成泛型便可和任意類型列表數據結合,而無須通過其他方式來達到復用的目的,將列表元素聲明成 anyRecord<string,any> 等類型。

先看不使用泛型情況下,如何實現這麼一個列表組件。此處只看列表元素的展示以闡述泛型的作用,其他邏輯比如數據加載等先忽略。

列表組件 List.tsx

interface Item {    [prop: string]: any;  }    interface Props {    list: Item[];    children: (item: Item, index: number) => React.ReactNode;  }    function List({ list, children }: Props) {    // 列表中其他邏輯...    return <div>{list.map(children)}</div>;  }

上面,為了儘可能滿足大部分數據類型,將列表的元素類型定義成了 [prop: string]: any; 的形式,其實和 Record<string,any> 沒差。在這裡已經可以看到類型的丟失了,因為出現了 any,而我們使用 TypeScript 的首要準則是盡量避免 any

然後是使用上面所定義的列表組件:

interface User {    id: number;    name: string;  }  const data: User[] = [    {      id: 1,      name: "wayou"    },    {      id: 1,      name: "niuwayong"    }  ];    const App = () => {    return (      <div className="App">        <List list={data}>          {item => {            // ? 此處 `item.name` 類型為 `any`            return <div key={item.name}>{item.name}</div>;          }}        </List>      </div>    );  };

這裡使用時,item.name 的類型已經成了 any。對於簡單數據來說,還可以接收這樣類型的丟失,但對於複雜類型,類型的丟失就完全享受不到 TypeScript 所帶來的類型便利了。

上面的實現還有個問題是它規定了列表元素必需是對象,理所應當地就不能處理元始類型數組了,比如無法渲染 ['wayou','niuwayong'] 這樣的輸入。

下面使用泛型改造上面的列表組件,讓它支持外部傳入類型。

interface Props<T> {    list: T[];    children: (item: T, index: number) => React.ReactNode;  }    function List<T>({ list, children }: Props<T>) {    // 列表中其他邏輯...    return <div>{list.map(children)}</div>;  }

改造後,列表元素的類型完全由使用的地方決定,作為列表組件,內部它無須關心,同時對於外部傳遞的 children 回調中 item 入參,類型也沒有丟失。

使用改造後的泛型列表:

interface User {    id: number;    name: string;  }  const data: User[] = [    {      id: 1,      name: "wayou"    },    {      id: 1,      name: "niuwayong"    }  ];    const App = () => {    return (      <div className="App">        <List list={data}>          {item => {            // ? 此處 `item` 類型為 `User`            return <div key={item.name}>{item.name}</div>;          }}        </List>        <List list={["wayou", "niuwayong"]}>          {item => {            // ? 此處 `item` 類型為 `string`            return <div key={item}>{item}</div>;          }}        </List>      </div>    );  };