跳到主要内容

类型操纵

TS 中有许多方法,来通过已有的类型定义一个新的类型。

泛型

function getLength<Type>(arr: Type[]) {
return arr.length;
}

let output = getLength([1, 2, 3, 4]); // => 4

目前(v4.7.4),箭头函数的类型参数如果为单个的话,会被 TS 识别为 JSX 标签,因此会报错。

具化一个泛型函数

可以通过类型标注或传入类型参数来具化一个泛型函数:

let getNumericArrayLength: (arg: number[]) => number = getLength;
let getBooleanArrayLength = getLength<boolean>;

泛型类

class SomeKind<Type> {
defaultValue?: Type;
add?: (x: Type, y: Type) => Type;
}

let myNumber = new SomeKind<number>();
myNumber.defaultValue = 0;
myNumber.add = function (x, y) {
return x + y;
};

在类中,类型参数是给实例用的,类自身的静态属性不可以使用类型参数。

类型约束

interface Identifiable {
id: string | number;
}

function printID<Data extends Identifiable>(data: Data) {
console.log(data.id);
}

printID({ id: 'zxcvb', name: 'Talaxy' }); // => 'zxcvb'

定义函数时应当考虑是否可以不用泛型,上述例子完全可以不用泛型。

一个类型参数可以约束另一个类型参数:

function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key];
}

let x = { a: 1, b: 2, c: 3 };

getProperty(x, 'a'); // => 1
getProperty(x, 'm'); // ERROR: `"m"` 类型参数不可以赋值给 `"a" | "b" | "c"` 类型

将类作为形参

function create<Kind>(c: { new (): Kind }): Kind {
return new c();
}

class Person {
name?: string;
}

const person = create(Person);
person.name = 'Talaxy';

keyof 类型操作符

keyof 操作符用于提取一个类型的键类型,它通常是一个字符串或数值字面量的联合类型:

type Point = { x: number; y: number };
type P = keyof Point; // `P` 等价于 `"x" | "y"`

如果类型有索引签名,keyof 则会直接返回索引签名的参数类型:

type SomeMap = { [k: string]: boolean };
type M = keyof SomeMap; // => `string | number`

typeof 类型操作符

除了原本在 JS 的功能,typeof 能够帮你提取值的类型:

const f = () => 'hello';
type F = typeof f; // => `() => string`

typeof 只能用在标识符(比如变量)上。

索引类型访问

可以用索引语法提取一个类型中某些属性的类型:

type Person = { age: number; name: string; alive: boolean };
type Age = Person['age']; // => `number`
type I1 = Person['age' | 'name']; // => `string | number`
type I2 = Person[keyof Person]; // => `string | number | boolean`
type I3 = Person['alve']; // => ERROR: `Person` 上不存在 'alve' 属性

你只能使用类型作为索引:

const key = 'age';
type Age = Person[key]; // ERROR: `key` 不能作为索引类型
type Age = Person[typeof key]; // `number`

面对数组时可以用 number 作为索引:

const people = [
{ name: 'Alice', age: 15 },
{ name: 'Bob', age: 23 },
{ name: 'Eve', age: 38 },
];

type Person = typeof people[number]; // => `{ name: string; age: number }`
type Age = typeof people[number]['age']; // => `number`

条件类型

条件类型的使用形式类似于三目条件表达式:

interface Animal {}

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

type Example1 = Dog extends Animal ? number : string; // => `number`
type Example2 = RegExp extends Animal ? number : string; // => `string`

条件类型可以创造出一些工具类型,比如:

// 用于平铺类型的工具类型
type Flatten<T> = T extends any[] ? T[number] : T;

type Str = Flatten<string[]>; // => `string`
type Num = Flatten<number>; // => `number`

使用推断

TS 提供了关键字 infer ,用于在条件类型中推断一个类型(通常是针对泛型):

type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
// 这是一个能获取函数返回值类型的工具类型
type ReturnType<Type> = Type extends (...args: never[]) => infer Return
? Return
: never;

type Str = ReturnType<(x: string) => string>; // => `string`

当对于重载函数这样有多个调用签名的,infer 会使用其最后一个签名:

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

type T1 = ReturnType<typeof stringOrNum>; // => `string number`

分配行为

对于这样的条件类型:

type ToArray<Type> = Type extends any ? Type[] : never;

当我们传入一个联合类型时,TS 会对其每个类型成员做转换,然后再将转换后的类型们联合起来:

// 类型为 `string[] | number[]`,而非 `(string | number)[]`
type StrArrOrNumArr = ToArray<string | number>;

如果要阻止这样的分配行为,可以在条件类型使用方括号:

type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;

type StrArrOrNumArr = ToArrayNonDist<string | number>; // => `(string | number)[]`

映射类型

有时候我们需要基于一个类型,来创建一个属性一一对应,但属性类型不同的类型。可以称之为映射类型。

映射类型通常会用到泛型,并且使用 keyof 来迭代每个属性:

// 这是一个能将类型的属性类型转为 `boolean` 的工具类型
type OptionsFlags<Type> = {
[Property in keyof Type]: boolean;
};

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

// 类型为 { darkMode: boolean; newUserProfile: boolean; }
type FeatureOptions = OptionsFlags<FeatureFlags>;

更改修饰符

映射的时候可以用 -+ 来去除或增加类型的修饰符,+ 是默认的:

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

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

属性名映射

可以用 as 语句直接修改属性名类型:

type EventConfig<Events extends { kind: string }> = {
[E in Events as E['kind']]: (event: E) => void;
};

type SquareEvent = { kind: 'square'; x: number; y: number };
type CircleEvent = { kind: 'circle'; radius: number };

// { square: (event: SquareEvent) => void; circle: (event: CircleEvent) => void; }
type Config = EventConfig<SquareEvent | CircleEvent>;

模板字面量类型

模板字面量类型基于字符串字面量类型:

type World = 'world';

type Greeting = `hello ${World}`; // => `hello world`

用联合实现组合:

type Role = 'user' | 'admin';

type ID = `${Role}_id`; // => `"user_id" | "admin_id"`

基于类型构建

// 一个用于监听对象属性的函数
declare function watch<Type>(obj: Type): {
// 这里需要限定属性类型为 `string` ,排除为 `symbol` 的可能
on(eventName: `${keyof Type & string}Changed`, callback: () => void): void;
};

const observer = watch({ name: 'Talaxy', age: 17 });

// OK
observer.on('nameChanged', () => console.log('name: changed'));
// ERROR: 不能将类型 `"scroll"` 赋值给 `"nameChanged" | ageChanged`
observer.on('scroll', () => console.log('scrolling'));

字面量变换

TS 提供了一些工具类型用来处理(字符串字面量)类型的字面量:

  • Uppercase<StringType> 字面量转大写
  • Lowercase<StringType> 字面量转小写
  • Capitalize<StringType> 开头大写
  • Uncapitalize<StringType> 开头小写
type EventName = 'change' | 'scroll' | 'move';

// "onChange" | "onScroll" | "onMove"
type EventType = `on${Capitalize<EventName>}`;

参考

Creating Types from Types - TypeScript