4

TYPESCRIPT 工具类型

 1 year ago
source link: https://www.fly63.com/article/detial/11806
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

类型是 TypeScript 的灵魂,很多时候我们需要种种新的类型。工具类型是 TypeScript 的一种特殊类型,为了解决某一特定的类型问题,得到一种新的类型。有的就是一种通用类型;有的则可以对现有类型进行一定的转换,从而得到一种新的类型。

TypeScript 内置了很多工具类型,熟练运用它们,可以让我们开发工作事半功倍。

下面我们将逐一介绍这些内置工具类型。

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类型用于定制化已有类型。

以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类型与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就是这个类型。这意味着我们不需要维护同一类型的两份拷贝,我们只有在函数中实际返回的那个类型。这无疑简化了我们的代码。

链接: https://www.fly63.com/article/detial/11806


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK