首页 TypeScript TypeScript 工具类型

TypeScript 工具类型

0 1.6K

类型是 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类型的firstNamestring类型的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

RecordUnion

有时候,我们的键值只是某些可选值的集合。例如:

const myData = {
    "uk" : { firstName: "John", lastName: "Doe" },
    "france" : { firstName: "Sarah", lastName: "Doe" },
    "india" : { firstName: "Jane", lastName: "Smith" }
}

我们假设数据集如上,所以键值只允许是ukfranceindia之一。这样的仅有有限值组成的集合类型叫做联合 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

PartialRequired正好相反,它的目的是把一种类型的所有属性都变成可选的。

我们还是使用上面的例子:

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,其可选值只有四个:ABCD。我们可以使用这种类型:

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类型,它包含四个可选值:ABCD。但是,在某些场景中,我们不希望值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包含四个属性:firstNamelastNameage以及lastActive。但我们不能保证User类型一直能够满足我们的需求:有些时候我们希望有一个新的类型,这个类型同User大致相同,只是少了agelastActive属性。那么,我们必须定义一个新的类型吗?不是。Omit就是为了满足这种需要。

Omit类型语法如下:

Omit<Type, Omissions>

其中,第一个泛型参数是Omit作用的类型,第二个参数是联合类型,表示需要忽略的属性。

例如:

type User = {
  firstName: string;
  lastName: string;
  age: number;
  lastActive: number;
}
 
type UserNameOnly = Omit<User, "age" | "lastActive">

上面代码中,UserNameOnly只包含了两个属性:firstNamelastNameagelastActive则被忽略。这样,我们就获得了可以使用的新类型。比如下面的代码:

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类型会将已有类型的nullundefined类型移除。

例如,我们有这样的类型:

type MyType = string | number | null | undefined

但是在某些场景中,我们不需要MyType类型中的nullundefined,那么,我们就可以使用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

ConstructorParametersParameters类型,区别在于,后者按照返回函数参数列表返回一个元组,而前者按照类型构造函数参数返回。

例如,ErrorConstructor声明如下:

new ErrorConstructor(message?: string): Error

那么,

type ErrorType = ConstructorParameters<ErrorConstructor>;
//   ^
//   └ - - 类型是 [message?: string]

ReturnType

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

发表评论

关于我

devbean

devbean

豆子,生于山东,定居南京。毕业于山东大学软件工程专业。软件工程师,主要关注于 Qt、Angular 等界面技术。

主题 Salodad 由 PenciDesign 提供 | 静态文件存储由又拍云存储提供 | 苏ICP备13027999号-2