类型是 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 = {
“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是值的类型。
上面代码中,我们定义了一种新的类型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 = {
“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 = {
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 = {
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 = {
firstName: “John”
}
Partial实际是把User类型转换成了:
type User = {
firstName?: string,
lastName?: string
}
与创建一个这样的类型不同,使用Partial类型,我们可以同时拥有普通的User类型以及完全可选的Partial类型。
Readonly
顾名思义,Readonly类型将一个类型变成只读的。
例如,在下面的代码中,我们不想任何人修改firstUser对象的值,就可以将firstUser类型设置为Readonly:
type User = {
firstName: string,
lastName: string
}
let firstUser:Readonly = {
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 = “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
第一个泛型参数是一个普通的联合类型,第二个泛型参数是需要排除的值。例如:
type MyUnionType = “A” | “B” | “C” | “D”
// 可以这么使用
let firstString:MyUnionType = “A”
// 错误:Type ‘”A”‘ is not assignable to type ‘”B” | “C” | “D”‘.
let secondString:Exclude = “A”
注意上面的secondString变量,其类型是排除了A之后的MyUnionType,因此,我们不能将A赋值给它。
如果需要排除多个值,可以使用|运算符,例如:
type MyUnionType = “A” | “B” | “C” | “D”
// 可以这么使用
let firstString:MyUnionType = “A”
let secondString:Exclude = “D”
// ^
// └ – – 类型是 “B” | “C” | “D”
let thirdString:Exclude = “D”;
// ^
// └ – – 类型是 “C” | “D”
let forthString:Exclude = “D”;
// ^
// └ – – 类型是 “D”
let lastString:MyUnionType = “A”
// ^
// └ – – 类型是 “A” | “B” | “C” | “D”
Exclude类型并不会改变原始的联合类型,这带给我们一种灵活性,即在某些场景中可以排除掉联合类型的某些值,但在另外的场景又可以使用完整的联合类型。
Extract
与Exclude类似,Extract类型同样适用于联合。Exclude是排除联合中的某些值,Extract则是选取其中的某些值。Extract语法如下:
Extract
我们来看一个例子:
type MyUnionType = “A” | “B” | “C” | “D”
let firstString:Extract = “A”
// ^
// └ – – 类型是 “A” | “B”
当我们使用Extract类型时,Extract会根据检查MyUnionType,看看其中是不是包含有”A” | “B”,如果存在,则返回一个新的联合类型。如果Extract给的值不存在,则新的值直接被忽略:
type MyUnionType = “A” | “B” | “C” | “D”
let firstString:Extract = “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
其中,第一个泛型参数是Omit作用的类型,第二个参数是联合类型,表示需要忽略的属性。
例如:
type User = {
firstName: string;
lastName: string;
age: number;
lastActive: number;
}
type UserNameOnly = Omit
上面代码中,UserNameOnly只包含了两个属性:firstName和lastName,age和lastActive则被忽略。这样,我们就获得了可以使用的新类型。比如下面的代码:
type User = {
firstName: string;
lastName: string;
age: number;
lastActive: number;
}
type UserNameOnly = Omit
type UserNameAndActive = Omit
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
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
// ^
// └ – – 类型是 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
let myArray:MyType = [ ‘hello ‘, ‘world’ ];
myFunction(…myArray)
Parameters类型实际简化了根据函数参数构建元组的方法。
既然是元组,我们就可以按照元组的方式去使用Parameters的返回值:
const myFunction = (a: string, b: string) => {
return a + b;
}
type AType = Parameters[0]
type BType = Parameters[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;
// ^
// └ – – 类型是 [message?: string]
ReturnType
ReturnType与Parameters类似,只不过ReturnType基于函数的返回值构建一种新的类型。
我们看一个例子:
function sendData(a: number, b: number) {
return {
a: ${a}
,
b: ${b}
}
}
type Data = ReturnType
// The same as writing:
// type Data = {
// a: string,
// b: string
// }
由于sendData()函数返回值类型是{ a: string, b: string },Data就是这个类型。这意味着我们不需要维护同一类型的两份拷贝,我们只有在函数中实际返回的那个类型。这无疑简化了我们的代码。
0 Comments