泛型類型 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 自動解析出 T 為 number ,此時對應的函數簽名為:
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>
);
};
一個更加真實的示例 一個更加實用的示例是列表組件。列表中的分頁加載,滾動刷新邏輯等,對於所有列表數據都是通用的,將這個列表組件書寫成泛型便可和任意類型列表數據結合,而無須通過其他方式來達到復用的目的,將列表元素聲明成 any 或 Record<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>
);
};
|