类型是 TypeScript 的灵魂,很多时候我们需要种种新的类型。工具类型是 TypeScript 的一种特殊类型,为了解决某一特定的类型问题,得到一种新的类型。有的就是一种通用类型;有的则可以对现有类型进行一定的转换,从而得到一种新的类型。
TypeScript 内置了很多工具类型,熟练运用它们,可以让我们开发工作事半功倍。
TypeScript 的工具类型文档:https://www.typescriptlang.org/docs/handbook/utility-types.html
本文出自:https://fjolt.com/series/typescript-utility-types,在此表示感谢
下面我们将逐一介绍这些内置工具类型。
Record
Record
是一种基于键值对的类型,可以创建固定格式的数据结构,用于组合复杂的数据类型。
假设有一个数据集如下:
const myData = { "123-123-123" : { firstName: "John", lastName: "Doe" }, "124-124-124" : { firstName: "Sarah", lastName: "Doe" }, "125-125-125" : { firstName: "Jane", lastName: "Smith" } }
这个数据集有一个string
类型的 ID 作为键,所有值类型都含有string
类型的firstName
和string
类型的lastName
两个字段。
对于这种数据类型,Record
是最适合的。我们可以这么定义类型:
type User = { firstName: string, lastName: string } const myData:Record<string, User> = { "123-123-123" : { firstName: "John", lastName: "Doe" }, "124-124-124" : { firstName: "Sarah", lastName: "Doe" }, "125-125-125" : { firstName: "Jane", lastName: "Smith" } }
Record
类型格式是Record<K, T>
,其中,K
是键的类型,T
是值的类型。
上面代码中,我们定义了一种新的类型User
,用于描述值类型,将键的类型指定为string
。
Record
和Union
有时候,我们的键值只是某些可选值的集合。例如:
const myData = { "uk" : { firstName: "John", lastName: "Doe" }, "france" : { firstName: "Sarah", lastName: "Doe" }, "india" : { firstName: "Jane", lastName: "Smith" } }
我们假设数据集如上,所以键值只允许是uk
、france
和india
之一。这样的仅有有限值组成的集合类型叫做联合 union。
在这个例子中,我们可以定义User
类型以及一个联合类型作为键:
type User = { firstName: string, lastName: string } type Country = "uk" | "france" | "india"; const myData:Record<Country, User> = { "uk" : { firstName: "John", lastName: "Doe" }, "france" : { firstName: "Sarah", lastName: "Doe" }, "india" : { firstName: "Jane", lastName: "Smith" } }
利用联合类型,我们就可以确保Record
的键为三个可选值之一。
Required
有时我们需要确保对象有一些属性是必须的,甚至这些属性是可选的,也必须给值。为了达到这一目的,TypeScript 提供了一个工具类型Required
。
默认情况下,我们在 TypeScript 中定义的新类型,所有属性都自动成为必须的:
type User = { firstName: string, lastName: string } let firstUser:User = { firstName: "John" }
上面代码中,firstUser
只有一个firstName
属性,没有lastName
,那么,TypeScript 会报错:
Property 'lastName' is missing in type '{ firstName: string; }' but required in type 'User'.
如果我们希望这个属性是可选的,那么,我们需要在类型定义中添加?
标注:
type User = { firstName: string, lastName?: string } let firstUser:User = { firstName: "John" }
例如上面的代码,我们把lastName
改成lastName?
,此时,lastName
就成为可选的,firstUser
也就能编译通过了。
至此,一切都很好。但是,现在又有一个问题:虽然在类型定义中,lastName
是可选的,但在某些情况下,我们需要User
类型必须提供lastName
,才能进行接下来的操作。为达到这一目的,我们可以使用下面的代码:
type User = { firstName: string, lastName?: string } let firstUser:User = { firstName: "John", } let secondUser:Required<User> = { firstName: "John" }
在这个例子中,secondUser
会报错:
Property 'lastName' is missing in type '{ firstName: string; }' but required in type 'Required'.
所以,当我们使用了Required
类型的时候,我们必须添加lastName
属性,这样就没有错误了:
type User = { firstName: string, lastName?: string } let secondUser:Required<User> = { firstName: "John", lastName: "Doe" }
这种机制看似多此一举,但带给我们更多的灵活性:我们可以针对系统中的某些函数进行特殊的建模,强制某些属性仅在某些场景是必须的。与其它工具类型一样,Required
可以针对interface
或对象类型使用,因为它是针对类型的,但不能对变量使用,不过这也没多大关系,因为对象不可能是空值(undefined
毕竟也是一种类型)。
Partial
Partial
与Required
正好相反,它的目的是把一种类型的所有属性都变成可选的。
我们还是使用上面的例子:
type User = { firstName: string, lastName: string } let firstUser:User = { firstName: "John" }
这段代码会报错:
Property 'lastName' is missing in type '{ firstName: string; }' but required in type 'User'.
因为firstUser
缺少了必须的属性lastName
。但是,如果在某些场景中,lastName
就是缺失的呢?我们就可以使用Partial
类型:
type User = { firstName: string, lastName: string } let firstUser:Partial<User> = { firstName: "John" }
Partial
实际是把User
类型转换成了:
type User = { firstName?: string, lastName?: string }
与创建一个这样的类型不同,使用Partial
类型,我们可以同时拥有普通的User
类型以及完全可选的Partial<User>
类型。
Readonly
顾名思义,Readonly
类型将一个类型变成只读的。
例如,在下面的代码中,我们不想任何人修改firstUser
对象的值,就可以将firstUser
类型设置为Readonly<User>
:
type User = { firstName: string, lastName: string } let firstUser:Readonly<User> = { firstName: "John", lastName: "Doe" }
这样,如果你要修改firstUser.firstName
或者firstUser.lastName
,就会直接报错:
Cannot assign to 'firstName' because it is a read-only property.
需要注意的是,Readonly
只针对于interface
或对象类型。一个变量的类型是Readonly
,只是这个对象的属性值是只读的,并不意味着这个对象是不可变的。例如:
let myVariable:Readonly<string> = "Hello World"; myVariable = "Goodbye World"; console.log(myVariable); // 输出 "Goodbye World"
虽然myVariable
类型是Readonly
,但我们仍旧可以给myVariable
赋一个新值。为了将myVariable
的引用本身设置为只读,我们需要使用const
关键字:
const myVariable:string = "Hello World"; // 错误: Cannot assign to 'myVariable' because it is a constant. myVariable = "Goodbye World";
Exclude
前面我们说过,联合就是有限值的集合。我们可以直接定义一个联合:
type MyUnionType = "A" | "B" | "C" | "D"
上面的例子中,我们定义了一个联合类型MyUnionType
,其可选值只有四个:A
、B
、C
、D
。我们可以使用这种类型:
type MyUnionType = "A" | "B" | "C" | "D" // 可以这么实用 let firstString:MyUnionType = "A" // 错误:Type '"some-string"' is not assignable to type 'MyUnionType'. let secondString:MyUnionType = "some-string"
理解了联合类型,我们就可以看看Exclude
了。
假设我们有一个MyUnionType
类型,它包含四个可选值:A
、B
、C
、D
。但是,在某些场景中,我们不希望值A
出现,那么就可以使用Exclude
类型。Exclude
语法如下:
Exclude<UnionType, ExcludedMembers>
第一个泛型参数是一个普通的联合类型,第二个泛型参数是需要排除的值。例如:
type MyUnionType = "A" | "B" | "C" | "D" // 可以这么使用 let firstString:MyUnionType = "A" // 错误:Type '"A"' is not assignable to type '"B" | "C" | "D"'. let secondString:Exclude<MyUnionType, "A"> = "A"
注意上面的secondString
变量,其类型是排除了A
之后的MyUnionType
,因此,我们不能将A
赋值给它。
如果需要排除多个值,可以使用|
运算符,例如:
type MyUnionType = "A" | "B" | "C" | "D" // 可以这么使用 let firstString:MyUnionType = "A" let secondString:Exclude<MyUnionType, "A"> = "D" // ^ // └ - - 类型是 "B" | "C" | "D" let thirdString:Exclude<MyUnionType, "A" | "B"> = "D"; // ^ // └ - - 类型是 "C" | "D" let forthString:Exclude<MyUnionType, "A" | "B" | "C"> = "D"; // ^ // └ - - 类型是 "D" let lastString:MyUnionType = "A" // ^ // └ - - 类型是 "A" | "B" | "C" | "D"
Exclude
类型并不会改变原始的联合类型,这带给我们一种灵活性,即在某些场景中可以排除掉联合类型的某些值,但在另外的场景又可以使用完整的联合类型。
Extract
与Exclude
类似,Extract
类型同样适用于联合。Exclude
是排除联合中的某些值,Extract
则是选取其中的某些值。Extract
语法如下:
Extract<Type, Union>
我们来看一个例子:
type MyUnionType = "A" | "B" | "C" | "D" let firstString:Extract<MyUnionType, "A" | "B"> = "A" // ^ // └ - - 类型是 "A" | "B"
当我们使用Extract
类型时,Extract
会根据检查MyUnionType
,看看其中是不是包含有"A" | "B"
,如果存在,则返回一个新的联合类型。如果Extract
给的值不存在,则新的值直接被忽略:
type MyUnionType = "A" | "B" | "C" | "D" let firstString:Extract<MyUnionType, "A" | "B" | "X"> = "A" // ^ // └ - - 类型是 "A" | "B",由于 "X" 不存在于 MyUnionType,直接被忽略
与Exclude
类似,Extract
也不会改变原始的联合类型。
Omit
Omit
类型用于定制化已有类型。
以User
类型为例:
type User = { firstName: string; lastName: string; age: number; lastActive: number; }
User
包含四个属性:firstName
、lastName
、age
以及lastActive
。但我们不能保证User
类型一直能够满足我们的需求:有些时候我们希望有一个新的类型,这个类型同User
大致相同,只是少了age
和lastActive
属性。那么,我们必须定义一个新的类型吗?不是。Omit
就是为了满足这种需要。
Omit
类型语法如下:
Omit<Type, Omissions>
其中,第一个泛型参数是Omit
作用的类型,第二个参数是联合类型,表示需要忽略的属性。
例如:
type User = { firstName: string; lastName: string; age: number; lastActive: number; } type UserNameOnly = Omit<User, "age" | "lastActive">
上面代码中,UserNameOnly
只包含了两个属性:firstName
和lastName
,age
和lastActive
则被忽略。这样,我们就获得了可以使用的新类型。比如下面的代码:
type User = { firstName: string; lastName: string; age: number; lastActive: number; } type UserNameOnly = Omit<User, "age" | "lastActive"> type UserNameAndActive = Omit<User, "age"> const userByName:UserNameOnly = { firstName: "John", lastName: "Doe", }; const userWithoutAge:UserNameAndActive = { firstName: "John", lastName: "Doe", lastActive: -16302124725 }
Pick
Pick
类型与Omit
相反:Omit
会忽略已有类型的某些属性,Pick
则是选取已有类型的某些属性。
来看下面的例子:
type User = { firstName: string; lastName: string; age: number; } type UserName = Pick<User, "firstName" | "lastName"> let user:UserName = { firstName: "John", lastName: "Doe" }
可以看到,Pick
从已有的User
类型生成了一个新的类型,我们可以直接使用新的类型。
NonNullable
NonNullable
类型会将已有类型的null
和undefined
类型移除。
例如,我们有这样的类型:
type MyType = string | number | null | undefined
但是在某些场景中,我们不需要MyType
类型中的null
和undefined
,那么,我们就可以使用NonNullable
类型:
type MyType = string | number | null | undefined type NoNulls = NonNullable<MyType> // ^ // └ - - 类型是 string | number
Parameters
Parameters
用于根据函数的参数生成一个新的类型。
假设我们有一个函数:
const myFunction = (a: string, b: string) => { return a + b; }
我们想要调用这个函数,方法有很多。其中一种是创建一个元组(tuple),使用展开运算符(...
)去调用:
const myFunction = (a: string, b: string) => { return a + b; } let passArray:[string, string] = [ 'hello ', 'world' ] // Returns 'hello world' myFunction(...passArray);
这里,我们定义了一个元组[string, string]
,然后给它赋值,最后使用展开运算符运行函数。
到目前为止,一切都很顺利。但是,如果myFunction
的参数变了呢?我们定义的[string, string]
都需要修改。Parameters
类型就是为了解决这一问题:
const myFunction = (a: string, b: string) => { return a + b; } type MyType = Parameters<typeof myFunction> let myArray:MyType = [ 'hello ', 'world' ]; myFunction(...myArray)
Parameters
类型实际简化了根据函数参数构建元组的方法。
既然是元组,我们就可以按照元组的方式去使用Parameters
的返回值:
const myFunction = (a: string, b: string) => { return a + b; } type AType = Parameters<typeof myFunction>[0] type BType = Parameters<typeof myFunction>[1] let a:AType = 'hello ' let b:BType = 'world' myFunction(a, b)
Parameters
也可以直接使用函数作为参数,例如:
type AnotherType = Parameters<(a: string, b: number) => void>
只不过这种直接使用匿名函数的方法并不怎么实用(因为仅仅是使用了匿名函数的声明,并没有定义)。
如果泛型参数不是一个函数,Parameters
会直接返回never
类型。
ConstructorParameters
ConstructorParameters
与Parameters
类型,区别在于,后者按照返回函数参数列表返回一个元组,而前者按照类型构造函数参数返回。
例如,ErrorConstructor
声明如下:
new ErrorConstructor(message?: string): Error
那么,
type ErrorType = ConstructorParameters<ErrorConstructor>; // ^ // └ - - 类型是 [message?: string]
ReturnType
ReturnType
与Parameters
类似,只不过ReturnType
基于函数的返回值构建一种新的类型。
我们看一个例子:
function sendData(a: number, b: number) { return { a: `${a}`, b: `${b}` } } type Data = ReturnType<typeof sendData> // The same as writing: // type Data = { // a: string, // b: string // }
由于sendData()
函数返回值类型是{ a: string, b: string }
,Data
就是这个类型。这意味着我们不需要维护同一类型的两份拷贝,我们只有在函数中实际返回的那个类型。这无疑简化了我们的代码。