在 TypeScript 中使用泛型
编写你的第一个泛型函数。
在使用 React 编写前端 UI 代码时,我们经常听到创建可复用组件的重要性。我们可以通过保持组件小巧,或以有条理的方式组合它们来实现这一点。例如,我们可以将那些可以修改的变量提取出来,并将其作为 props,从而使组件能够适用于各种场景。
我们同样应该努力在代码库中的非组件代码——即我们构建的函数和类(无论是与 React 组件并行还是独立存在)——也实现类似的目标。在大型应用中,许多功能常常在多个页面、不同区域频繁复用,而且最重要的是,它们需要适用于多种不同的数据类型。在本课中,我们将探讨泛型如何帮助我们在应用程序中复用诸如搜索、排序和过滤等宝贵的功能。
第一个示例
这是一个简化的泛型排序示例。
注意:为了跟上本课程的后续内容,请在由
create-react-app生成的应用程序的src/目录下新建一个文件夹generics/,并创建一个新文件generics.ts。有关如何创建create-react-app的说明,请参见课程附录。在本课程的其余部分,我们应在支持 IntelliSense 的编辑器中编写 TypeScript 文件。课程末尾将提供一个完整的实时应用示例,可用于验证我们的工作成果。
首先,定义一个简单的接口 IFooBar:
1interface IFooBar { 2 foo: string; 3 bar: string; 4}
该接口仅包含两个属性:foo 和 bar,两者均为字符串类型。
接下来,为这个类型关联一些真实数据。定义一个 const fooBars,其类型为 Array<IFooBar>。我们将填充易于理解的数据,以便在课程后期轻松验证我们的排序函数是否正常工作:
1const fooBars: Array<IFooBar> = [ 2 { 3 foo: "Foo C", 4 bar: "Y Bar" 5 }, 6 { 7 foo: "Foo B", 8 bar: "Z Bar" 9 }, 10 { 11 foo: "Foo A", 12 bar: "X Bar" 13 } 14]
假设在我们的应用中某处,我们需要对类型为 IFooBar 的数据进行排序。在这种情况下,我们可能从 API 接口获取一个 IFooBar 数组。为了排序数据,我们可以编写一个 sortByFoo 函数,按照以下步骤进行:
- 首先,接收
fooBars数组。 - 然后,使用 JavaScript 内置的
sort方法,显式地根据foo属性进行排序。 - 最后,对数组进行原地排序(
sort会直接修改原数组)。
1function sortByFoo(fooBars: Array<IFooBar>) { 2 fooBars.sort((a, b) => { 3 if (a.foo > b.foo) { 4 return 1; 5 } 6 if (a.foo < b.foo) { 7 return -1; 8 } 9 return 0; 10 }) 11}
如果我们要按另一个属性 bar 排序,也可以写出类似的 sortByBar() 函数:
1function sortByBar(fooBars: Array<IFooBar>) { 2 return fooBars.sort((a, b) => { 3 if (a.bar > b.bar) { 4 return 1; 5 } 6 if (a.bar < b.bar) { 7 return -1; 8 } 9 return 0; 10 }) 11}
这些解决方案对于仅包含 foo 和 bar 两个属性的数据是有效的,但很容易想象到更复杂的数据类型,它们拥有数十个不同的属性。为每个属性都编写显式的 sort 函数存在两个问题:
- 耗时极多。
- 导致大量重复代码,执行几乎相同的任务(排序)。
引入泛型
这种情况正是 TypeScript 泛型的理想应用场景。我们可以创建一个名为 sortByKey 的泛型函数,它不仅能替代 sortByFoo 和 sortByBar,还便于未来扩展。例如,我们可以在 IFooBar 中添加一个新属性 hello。
1interface IFooBar { 2 foo: string; 3 bar: string; 4 hello: string; // 新增属性!糟糕,我们还没为此编写排序函数! 5}
现在,让我们学习如何编写这个泛型函数!我们需要使用尖括号语法(< 和 >)向 TypeScript 表明正在使用泛型。泛型的一个常见模式是使用大写字母 T 来表示需要提供的泛型类型。因此,我们可以在函数名后添加 <T> 开始定义排序函数:
1function sortByKey<T>() { 2 3}
当开发者需要多个泛型类型时,最常见的做法是继续使用字母表中的下一个大写字母,如 U 和 V,用逗号分隔。例如,如果 sortByKey 需要三个泛型类型,函数签名可以写作 sortByKey<T, U, V>。
在 React 的类组件类型中可以找到泛型的真实应用示例,例如代码行 class React.Component<P = {}, S = {}, SS = any>。在这个例子中,React 团队采用了更直观的命名约定:虽然仍使用大写字母,但不再使用 T、U 或 V,而是使用与每个泛型变量含义相关的字母——这里 P 代表 props,S 代表 state,SS 代表 snapshot。
1interface IAppProps { 2 3} 4 5interface IAppState { 6 7} 8 9interface IAppSnapShot { 10 11} 12 13export default class App<IAppProps, IAppState, IAppSnapShot> { 14 15}
这个例子稍显高级,因为很少会同时看到或需要三种不同的泛型类型。通常,单个泛型类型就足以满足大多数功能的复用需求。
现在,我们需要为函数添加参数。在 sortByFoo 和 sortByBar 中,我们明确指定了参数类型 Array<IFooBar>。现在我们希望使用泛型类型 T 作为参数类型。换句话说,我们的函数应能处理任意类型 T 的数组。在 TypeScript 中,这表示为 Array<T>。由于这是泛型类型,它可以代表任何类型,因此使用一个通用名称是合理的。我们将其命名为 data,并作为 sortByKey 函数的第一个参数。
1function sortByKey<T>(data: Array<T>) { 2 3}
keyof 操作符
我们仍然需要添加传递键名以进行排序的能力。再次借助 TypeScript 的强大功能,我们可以使用其 keyof 类型操作符。keyof 类型会生成一个字面量联合类型,表示某个类型的键。在此情况下,我们将使用我们的泛型类型 T。TypeScript 足够智能,即使在泛型类型上也能使用 keyof 操作符。让我们完成 sortByKey 函数的签名:
1function sortByKey<T>(data: Array<T>, key: keyof T) { 2 3}
现在编写函数体。sortByKey 的实现与 sortByFoo 或 sortByBar 的区别不大,只是需要将显式使用的 foo 或 bar 键替换为我们通用的键变量。由于我们使用了 keyof T,TypeScript 不会在访问 a[key] 或 b[key] 时报错,因为 key 确实是 T 的键之一。
1function sortByKey<T>(data: Array<T>, key: keyof T) { 2 return data.sort((a, b) => { 3 if (a[key] > b[key]) { 4 return 1; 5 } 6 if (a[key] < b[key]) { 7 return -1; 8 } 9 return 0; 10 }) 11}
大功告成!我们现在可以通用地对应用中任意类型的数据进行排序!
双重优势
通过使用泛型,我们的函数不仅在整个应用中具备高度复用性,还能防止运行时错误。
例如,下面这两行代码都能正常工作。TypeScript 不会产生错误,因为 foo 和 bar 都是 IFooBar 接口的属性。
1// 两者都没问题:foo 和 bar 都是 IFooBar 的属性! 2sortByKey<IFooBar>(fooBars, "foo") 3sortByKey<IFooBar>(fooBars, "bar")
但如果尝试按 cat 属性对 fooBars 排序,TypeScript 会立即用红色标出 cat。
1// TypeScript 报错:cat 不是 IFooBar 的属性! 2sortByKey<IFooBar>(fooBars, "cat")
悬停在错误上会显示警告信息。点击下方交互示例中的“运行”按钮查看错误,然后注释掉最后一行代码,即可看到 sortByKey 函数的实际效果。
generics.ts
1interface IFooBar { 2 foo: string; 3 bar: string; 4} 5 6const fooBars: Array<IFooBar> = [ 7 { 8 foo: "Foo C", 9 bar: "Y Bar" 10 }, 11 { 12 foo: "Foo B", 13 bar: "Z Bar" 14 }, 15 { 16 foo: "Foo A", 17 bar: "X Bar" 18 } 19] 20 21function sortByFoo(fooBars: Array<IFooBar>) { 22 fooBars.sort((a, b) => { 23 if (a.foo > b.foo) { 24 return 1; 25 } 26 if (a.foo < b.foo) { 27 return -1; 28 } 29 return 0; 30 }) 31} 32 33function sortByBar(fooBars: Array<IFooBar>) { 34 fooBars.sort((a, b) => { 35 if (a.bar > b.bar) { 36 return 1; 37 } 38 if (a.bar < b.bar) { 39 return -1; 40 } 41 return 0; 42 }) 43} 44 45function sortByKey<T>(data: Array<T>, key: keyof T): Array<T> { 46 return data.sort((a, b) => { 47 if (a[key] > b[key]) { 48 return 1; 49 } 50 if (a[key] < b[key]) { 51 return -1; 52 } 53 return 0; 54 }) 55} 56 57// 测试我们的函数,并将结果输出到控制台。 58 59// 两处调用均无问题:foo 和 bar 都是 IFooBar 的属性! 60console.log("按 'foo' 排序:"); 61console.log(sortByKey<IFooBar>(fooBars, "foo")); 62 63console.log("按 'bar' 排序:"); 64console.log(sortByKey<IFooBar>(fooBars, "bar")); 65 66// TypeScript 报错:cat 不是 IFooBar 的属性! 67// (注释掉此行以正确查看上述结果) 68console.log(sortByKey<IFooBar>(fooBars, "cat"));
如果我们使用原生 JavaScript,就不会看到这种提示。我们可能直到运行时才会发现问题,甚至可能困惑于为何按 cat 属性排序根本不会改变列表顺序!
泛型真棒!
泛型是不是很强大?这只是使用 TypeScript 泛型潜力的开始而已!