今天我们来总结一下 TypeScript 泛型的定义和使用,并列举几个例子。
在创建我自己的 React 造轮子项目的时候,我发现如果能够合理使用 TypeScript 泛型,就可以在很大程度上提高开发的效率。
因为当我们自定义组件的时候,发现如果组件支持的数据类型是写死的话,那么当我们在重用组件的时候就必须要使用限定的数据类型。
那么如果用户使用我们的组件,我们能不能让组件能够接受用户传进来的各种数据类型呢?这就用到了 TS 中的泛型。
泛型定义
我认为泛型设计主要是针对 TS 中不同成员之间数据类型的约束而言的,我们使用泛型可以让一些自定义组件的可重用性大大增加,使一个组件能够支持多种类型的数据,这样就可以让用户以自己的数据类型来使用组件。
简单例子
当我们定义一些普通类型的时候,又想它兼容多种类型的数据,一般可以这样写:
type A = 'hi' | 123
那么类型 A 就可以支持字符串 'hi' 和 number 123
我们也可以定义一个函数,它接受两个参数,都是 number 类型,然后返回这两个参数的和,结果也是 number:
const add = (a:number, b:number)=> a + b
当然,除了接受 number,这样的函数也应该可以支持 string 等不同的类型,那我们可不可以将这种函数抽成类型呢?当然:
const Add<T> = (a: T, b: T) => T
其中 T 代表 type,用来定义泛型时作为第一个类型变量的名称,当然这只是约定俗成的写法。
那么我们在调用函数的时候,就可以自己传任意的类型:
const addN: Add<number> = (a, b) => a+b //返回 number
const addS: Add<string> = (a, b) => a + ' ' + b //返回 string
而且泛型也可以用在接口 Interface 和类 class 中,这里也举两个例子:
interface Identities<V, M> = {
value: V,
message: M
}
fucntion identity<T, U>(value: T, message: U): Identities<T, U>{
let identities: Identities<T, U> = {
value,
message
}
return identities
}
interface GenericInterface<U>{
value: U,
getIdentity: ()=> U
}
class IdentityClass<T> implements GenericInterface<T>{
value: T
construcrtor(value: T){
this.value = value
}
getIdentity():T {
return this.value
}
}
const myNumberClass = new IdentityClass<number>(66)
const myStringClass = new IdentityClass<string>("Hello world!")
React 中的用法
至于在 React 中的用法也基本相同:
const Props = {
name: string
}
const App: FunctionComponent<Props> = (props)=>{
props
return <div>hi</div>
}
泛型约束、重载
但是在使用泛型的过程中,也很容易发现问题,比如我举一个例子:
type Add<T> = (a: T, b: T) => T
const add: Add<number|string> = (a, b)=>{
...
}
我们发现,这时候我们没有办法限定 a 和 b 的类型一致,也就是说可能用户在传入参数的时候 a 是 number,而 b 则是一个 string,但是 JS 并不会报错,只有当函数执行完之后才会报错说字符串和数字不能相加。
那么有没有方法可以限定传参的类型呢?
我们可以尝试使用重载方法:
function add2 (a:number, b:number): number;
function add2 (a:string, b:string): string;
function add2 (a:any, b:any): any {
if(typeof a === 'number' && typeof b === 'number'){
return a+b
}else{
return a+ ' ' + b
}}
add2('hi',1) //报错,第一个重载不能把string赋给number,第二个反过来,第三个没意义
我们也可以使用这个方法来尝试实现一些 AJAX 方法(初步实现,功能有缺陷):
axios.get('url',{header:'get'})
axios.get({url : '/xxx',header:'get'})
type Options = {header: any}
function get(url:string, options?:Options): void
function get(options:{url:string} & Options): void
function get(url:string | (Options & {url:string}),options?:Options):any{ if(arguments.length === 1){
const myOptions = url as {options:{url:string} & Options} myOptions.url
}else{
console.log(url as string)
} }
type User = {
id: string|number;
name: string;
age: number
}
type Response<T> = {
data: T
}
type CreateResource = (path:string)=>{
create:(attrs: Omit<Partial<User>, 'id'>) => Promise<Response<User>>;
delete:(id: User['id']) => Promise<Response<never>>;
update:(id: User['id'],attrs:Omit<Partial<User>, 'id'>) => Promise<Response<User>>;
get:(id: User['id']) => Promise<Response<User>>;
getPage:(page:number) => Promise<Response<User[]>>
}
const createResource:CreateResourse = (path)=>{
return{
create(attrs){
const url = path + 's'
axios.post<User>(url,{data:attrs})
},
delete(){},
update(){},
get(){},
getPage(){}
}
}
var userResource = createResource('api/v1/user')
总结
总体来说,使用泛型可以极大程度增加我们自定义组件的可重用性,在写项目的过程中,如果能写出一个接受泛型的组件,那么我们在调用这类型的组件的时候就会更加灵活,创造性也更多。
就拿我自己的 react 造轮子项目来说,我有如下体会:
- 虽然能实现的功能不多,但是在有限的功能中,泛型极大程度上方便了我对自定义组件的类型进行约束
- 使用泛型有助于我在每一次调用组件和函数时弄清楚参数的数据类型,加深理解
- 使用泛型能够简化我的代码,相当于把重复的代码提取出来,每次调用只用写出不同的参数数据类型即可
当然,本篇总结也只是讲出了泛型的很小一部分基础和应用方法,包括在最后一个例子中,我也尝试运用了一些 React 自带的泛型工具,如 Omit、Partial等,此外还有 Record、Pick、Exclude 等非常有效的工具也能提高我们对泛型的应用。