Skip to main content

Advanced Types

typeof类型操作符

在JavaScript中,typeof操作符是用来获取一个值的类型,而在TypeScript中,typeof可以用来获取一个变量或属性的类型,是用来进行类型访问的工具:

let aaa = 'hello';

// string
let nnn: typeof aaa;

这对于基本类型来说不是很有用,但是与其他类型运算符结合使用,可以使用typeof方便地表达许多模式。

例如,可以获取一个函数的返回类型:

function fun() {
return {
x: 10,
y: 20
}
}

// 'fun' refers to a value, but is being used as a type here.
type MyReturn = ReturnType<fun>

如上,会报错,原因是值和类型不一样,为了得到fun函数的值的类型,可以使用typeof

/*
type MyReturn = {
x: number;
y: number;
}
*/
type MyReturn = ReturnType<typeof fun>

TypeScript明确地限制了typeof的是使用范围,只能在标识符或它们的属性上使用,这有助于避免编写正在执行但实际上并未执行的代码的令人困惑的陷阱:

function msgbox(s) {
return s
}
// "msgbox": Unknown word.
let shouldContinue: typeof msgbox("Are you sure you want to continue?");

keyof类型操作符

keyof运算符获取一个对象类型,并生成一个字符串或数字字面量联合类型,包含其键:

interface UserInfo {
name: string;
email: string;
age: number;
}

// 'name' | 'email' | 'age'
type UserInfoKeys = keyof UserInfo;

如果类型存在stringnumber索引签名,则返回对应的类型:

type Arrayish = { [n: number]: unknown };
type A = keyof Arrayish;

type Mapish = { [k: string]: boolean };
type M = keyof Mapish;

当与映射类型结合使用时,keyof类型变得特别有用。

映射类型

TypeScript中,映射类型是基于已存在的类型创建一种新的类型,已存在类型的每个属性都以某种方式进行转换。当不想重复声明一些类型的时候,映射类型很有用,其建立在索引签名的语法至上。

一个映射类型是一种泛型,使用对象属性键名的联合类型来迭代键以创建新的类型,使用一个keyof操作符声明:

type OptionFlags<Type> = {
[k in keyof Type]: boolean;
}

type FeatureFlags = {
name: string;
darkMode: () => void;
}

/*
type FeatureOptions = {
name: boolean;
darkMode: boolean;
}
*/
type FeatureOptions = OptionFlags<FeatureFlags>;

如上,OptionFlags将接受Type类型的所有属性,并将值类型标记为boolean

映射修饰符

在映射类中种,有两个额外的修饰符:readonly? ,分别影响可变性和可选性。也可以通过前缀+-添加或移除修饰符,如果没有指明,则默认为+

type CreateMutable<Type> = {
-readonly [Property in keyof Type]-?: Type[Property];
}

type LockedAccount = {
readonly id: string;
readonly name: string;
age?: number;
}
/*
type UnlockedAccount = {
id: string;
name: string;
age: number;
}
*/
type UnlockedAccount = CreateMutable<LockedAccount>;

as二次映射

从TypeScript 4.1版本开始,可以使用映射类型中的as子句重新映射映射类型中的键:

type Getters<Type> = {
[Property in keyof Type as `get${Capitalize<string & Property>}`]: () => Type[Property]
}

interface Person {
name: string;
age: number;
location: string;
}

/*
type LazyPerson = {
getName: () => string;
getAge: () => number;
getLocation: () => string;
}
*/
type LazyPerson = Getters<Person>;

也可以通过as过滤成员:

type RemoveKindField<Type> = {
[Property in keyof Type as Exclude<Property, 'radius'>]: Type[Property]
}

interface CircleOptions {
kind: "circle";
radius: number;
}

/*
type KindlessCircle = {
kind: "circle";
}
*/
type KindlessCircle = RemoveKindField<CircleOptions>;

可以映射任意联合,而不仅仅是string | number | symbol,但任何类型的联合:

interface SquareEvent {
kind: "square";
x: number;
y: number;
}

interface CircleEvent {
kind: "circle";
radius: number;
}

type ShapeEvent<Events extends { kind: string}> = {
[E in Events as E['kind']]: (e: Event) => void
}

/*
type Config = {
square: (e: Event) => void;
circle: (e: Event) => void;
}
*/
type Config = ShapeEvent<SquareEvent | CircleEvent>;

结合条件类型

映射类型可结合条件类型来设置属性的的类型:

type DBFields = {
id: { format: 'incrementing' };
name: { type: string; pii: string };
}

type ExtractPII<Type> = {
[Property in keyof Type]: Type[Property] extends { pii: true } ? true : false;
}

type ObjectsNeedingGDPRDeletion = ExtractPII<DBFields>;

条件类型

条件类型描述输入与输出类型之间的关系。

interface Animal {
woof: () => void;
}

interface Dog extends Animal {
live(): void;
}

// type TargetType = number
type TargetType = Dog extends Animal ? number : string;

条件类型看起来有点像条件表达式,其格式如下:

SomeType extends OtherType ? TrueType : FalseType

extends左边的类型可赋值给右边的时候,会得到true分支,否则会得到false分支。

有时候,可以使用条件类型来简化函数重载,考虑如下示例:

interface IdLabel {
id: number;
}

interface NameLabel {
name: string;
}

function createLabel(id: number): IdLabel;
function createLabel(name: string): NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel {
throw "unimplemented";
}

// 使用条件类型改造
type IdOrName<T extends number | string> = T extends number ? IdLabel : NameLabel;

function createLabel<T extends number | string>(idOrName: T): IdOrName<T> {
throw "unimplemented";
}

条件类型限制

通常,条件类型中的检查将为我们提供一些新信息。 就像使用类型保护缩小范围可以为我们提供更具体的类型一样,条件类型的true分支将通过我们检查的类型进一步限制泛型。

// Type '"message"' cannot be used to index type 'T'.
type MessageOf<T> = T['message']

在此示例中,TypeScript 出错,因为T不知道是否有名为message的属性。 我们可以限制T,TypeScript将不会抛出错误:

type MessageOf<T extends { message: unknown }> = T['message']

type T1 = MessageOf<{ message: string }>

但是,如果希望MessageOf采用任何类型,并且在消息属性不可用时默认为never之类的类型,该怎么办? 我们可以通过移出约束并引入条件类型来做到这一点:

type MessageOf<T> = T extends { message: unknown } ? T['message'] : never

interface Email {
message: string
}

interface Address {
unit: string
}

// type EmailContent = string
type EmailContent = MessageOf<Email>

// type AddressContent = never
type AddressContent = MessageOf<Address>

条件类型推断

条件类型提供了一种使用infer关键字从true分支中比较的类型进行推断的方法。例如,可以推断Flatten中的元素类型,而不是使用索引访问类型“手动”取出它:

type Flatten<T> = T extends Array<infer Item> ? Item : T

如上,使用infer关键字以声明方式引入一个名为Item的新泛型类型变量,而不是指定如何在true分支中检索T的元素类型。这使我们不必考虑如何挖掘和探究我们感兴趣的类型的结构。

通过可以通过infer封装一些通用工具类型别名:

type GetReturnType<T> = T extends (...args: never[]) => infer Return ? Return : never

type Num = GetReturnType<() => number>
type Str = GetReturnType<(x: string) => string>

当从具有多个调用签名的类型(例如重载函数的类型)进行推断时,将从最后一个签名进行推断:

function stringOrNum(x: string): number;
function stringOrNum(x: number): string;
function stringOrNum(x: string | number): string | number;

// type T = string | number
type T = ReturnType<typeof stringOrNum>

分配条件类型

文本类型分配:

type Bool1 = 'a' extends 'a' ? true : false
type Bool2 = 'a' | 'b' extends 'a' ? true : false
type Bool3 = 'a' extends 'a' | 'b' ? true : false

当条件类型作用于泛型类型时,如果给定联合类型,它们就会变得具有分布式性:

type Bool<T> = T extends 'a' ? string : number;
// type Bool2 = string | number
type Bool2 = Bool<'a' | 'b'>

type toArray<T> = T extends any ? T[] : never
// type MyArray = string[] | number[]
type MyArray = toArray<number | string>
/*
toArray<number | string>
toArray<number> | toArray<string>
number[] | string[]
*/

分配性是期望的行为,但是如果需要避免这种行为,可以用方括号将extends关键字的每一侧括起来:

type toArray<T> = [T] extends [any] ? T[] : never
// type MyArray = (string | number)[]
type MyArray = toArray<number | string>

type toArray<T> = [T] extends [number] | [string] ? T[] : never
// type MyArray = never
type MyArray = toArray<number | string>

如果泛型类型传入never,则不会进行泛型检查,直接返回never

type Bool<T> = T extends 'a' ? string : number;
// type Bool2 = never
type Bool2 = Bool<never>

索引访问类型

我们可以使用索引访问类型来查找另一种类型的特定属性:

type PersonInfo = {
name: string;
age: number;
alice: boolean
}

// type Age = number
type Age = PersonInfo['age']

索引本身是一种类型,因此可以使用联合、keyof或其他类型:

// type T1 = string | number
type T1 = PersonInfo['age' | 'name'];

// type T2 = string | number | boolean
type T2 = PersonInfo[keyof PersonInfo];

如果尝试对不存在的属性建立索引,您甚至会看到错误:

// Property 'address' does not exist on type 'PersonInfo'.
type T3 = PersonInfo['address']

使用任意类型进行索引的另一个示例是使用number来获取数组元素的类型。 可以将其与typeof结合起来,以方便地捕获数组字面量的元素类型:

const MyArray = [
{ name: 'Alice', age: 14 },
{ name: 'Bob', age: 15 }
]

type PersonInfo = typeof MyArray[number]
type PersonAge = typeof MyArray[number]['age']

还有一点是,只能在索引时使用类型而非值,这意味着您不能使用const来进行变量引用:

const key = 'age'
// Type 'key' cannot be used as an index type.ts
type PersonAge = Person[key]

type key = 'age'
type PersonAge = Person[key]

字面量类型

除了一般的字符串和数字类型外,我们还可以在类型位置引用特定的字符串和数字。思考这个问题的一种方法是考虑 JavaScript 如何使用不同的方式来声明变量。varlet都允许更改变量内保存的内容,而const则不允许。 这反映在 TypeScript 如何为文字创建类型上。

let changeString = 'hello';
changeString = 'world'

const constantString = 'hello world'

就其本身而言,文字类型并不是很有实际意义:

let xc: "hello" = "hello";
// OK
xc = "hello";
// Type '"howdy"' is not assignable to type '"hello"'.
xc = "howdy";

拥有一个只能有一个值的变量并没有多大用处。但是通过将字面量类型组合成联合,可以表达更有用的概念。例如,仅接受一组特定已知值的函数:

function printText(s: string, alignment: 'left' | 'center' | 'right') {
// ...
}

printText('text-align', 'left')
// Argument of type '"middle"' is not assignable to parameter of
// type '"left" | "center" | "right"'.
printText('text-align', 'middle')

当然,还可以和其它非字面量类型合并:

interface Options {
width: number;
}
function configure(x: Options | "auto") {
// ...
}
configure({ width: 100 });
configure("auto");
// Argument of type '"automatic"' is not assignable to parameter of type 'Options | "auto"'.
configure("automatic");

还有另一种字面量类型:布尔字面量。 只有两种布尔字面量类型,它们是truefalse类型。 boolean类型本身实际上只是联合类型的true | false的别名。

字面量推断

当用一个对象初始化一个变量时,TypeScript会假设对象的属性后面可能会改。如下示例:

const obj = {
counter: 1
}

if (true) {
obj.counter = 2
}

如上,TypeScript会认为counter是可变的,但是必须是number类型。

相同的规则可运用到string,如下:

function handleRequest(url: string, method: 'GET' | 'POST') {
// Fetching data
}

const req = {
url: 'https://www.test.com',
method: 'POST'
}

// Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'
handleRequest(req.url, req.method)

上述示例中,将req.method推断为string而非POST,由于类型不兼容,因此会报错。可采用如下方法解决此问题:

  1. 类型断言
handleRequest(req.url, req.method as 'POST')
  1. as const
const req = {
url: 'https://www.test.com',
method: 'POST'
} as const;

handleRequest(req.url, req.method)

模板字面量类型

模板字面量基于字符串字面量类型,并且可以通过联合类型拓展到许多字符串类型。它跟JavaScript中的模板字符串有相同的语法,但是被用在类型位置。

当与具体字面量类型一起使用时,模板字面量通过连接内容生成新的字符串字面量类型:

type hello = 'hello'
// type Greeting = "hello world"
type Greeting = `${hello} world`

当在插入位置使用联合时,类型是每个联合成员可以表示的每个可能的字符串字面量的集合:

type locale = 'zh' | 'en'
type info = 'name' | 'address'

// type UserInfo = "user_zh" | "user_en" | "user_name" | "user_address"
type UserInfo = `user_${locale | info}`

联合字符串

考虑如下示例,如果想分别监听对象中每个属性的改变,该如何声明类型:

const person = makeWatchedObject({
firstName: 'Tom',
lastName: 'Jason',
age: 12
})

person.on('firstNameChange', (newValue) => {
console.log(newValue);
})

此例中,可使用联合字符类来声明类型:

type PropEventSource<T> = {
on: (eventName: `${string & keyof T}Change`, cb: (newValue: unknown) => void ) => void
}

declare function makeWatchedObject<T>(obj: T): T & PropEventSource<T>;

const person = makeWatchedObject({
firstName: 'Tom',
lastName: 'Jason',
age: 12
})

person.on('firstNameChange', (newValue) => {
console.log(newValue);
})

除此之外模板字面量还可进行类型推断:

type PropEventSource<T> = {
on<Key extends string & keyof T>(eventName: `${Key}Change`, cb: (newValue: T[Key]) => void): void
}

递归类型

在TypeScript中,递归类型是定义一种自我引用的类型的方式。它通常用来定义复杂的数据解构,如树或者链表。

type IteratorType<T> = {
value: T,
next: IteratorType<T> | null
}

const list: IteratorType<number> = {
value: 1,
next: {
value: 2,
next: {
value: 3,
next: null
}
}
}