22

初探 TypeScript 类型编程

 3 years ago
source link: https://segmentfault.com/a/1190000024495646
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.

本文首发于我的 博客 ,转载请注明出处: http://kohpoll.github.io/blog...

平常我们编写 TypeScript 时,主要会使用类型注解(给变量、函数等加上类型约束),这可以增强代码可读性、避免低级 bug。实际上 TypeScript 的类型系统设计的非常强大,强大到可以单独作为一门编程语言。本文是自己学习 TypeScript 类型编程的一个总结,希望对你有帮助。

开始之前

本文不会对 TypeScript 的基础语法和使用进行说明,你可以参考互联网上提供的优秀资料:

启程

参考 SCIP 中对于编程语言的描述。一门编程语言应该提供以下机制:

  • 基本表达式。用来表示语言所关心的最简单的个体。
  • 组合的方法。从简单的个体出发构造复合的对象。
  • 抽象的方法。能将复合对象封装作为独立单元去使用。

下面我们将以这三个方面为线索来探索 TypeScript 的类型编程。

基本表达式

我们首先来看看类型编程中,定义“变量”的方式:

// string、number、boolean 的值可以作为类型使用,称为 literal type

type LiteralS = 'x';

type LiteralN = 9;

type LiteralB = true;

// 基础类型

type S = string;

// 函数

type F = (flag: boolean) => void;

// 对象

type O = { x: number; y: number; };

// tuple

type T = [string, number];

这里稍微补充下 interface 和 type 的区别。

最主要的区别就是 type 可以进行“类型编程”,interface 不行。

interface 能定义的类型比较局限,就是 object/function/class/indexable:

// object

interface Point {

x: number;

y: number;

}

const p: Point = { x: 1, y: 2 };

// function

interface Add {

(a: number, b: number): number;

}

const add: Add = (x, y) => x + y;

// class

interface ClockConstructor {

new (hour: number, minute: number): ClockInterface;

}

interface ClockInterface {

tick(): void;

}

const Clock: ClockConstructor = class C implements ClockInterface {

constructor(hour: number, minute: number) { return this; }

tick() {}

}

const c = new Clock(1, 2);

c.tick();

// indexable

interface StringArray {

[index: number]: string;

}

interface NumberObject {

[key: string]: number;

}

const s: StringArray = ['a', 'b'];

const o: NumberObject = { a: 1, b: 2 };

interface 可以被重新“打开”,同名 interface 会自动聚合,非常适合做 polyfill。比如,我们想要在 window 上扩展一些原本不存在的属性:

interface Window {

g_config: {

locale: 'zh_CN' | 'en_US';

};

}

组合的方法

有了基本表达式,我们来看组合的方法。

| 和 & 操作符

& 表示必须同时满足多个契约,| 表示满足任意一个契约即可。

type Size = 'large' | 'normal' | 'small';

// never 可以理解为 | 运算的“幺元”,即:x | never = x

type T = 1 | 2 | never; // 1 | 2

type Animal = { name: string };

type Flyable = { fly(): void };

type FlyableAnimal = Animal & Flyable; // { name: string, fly(): void }

keyof 操作符

interface Sizes {

large: string;

normal: string;

small: string;

x: number;

}

// 获取对象的属性值

type Size = keyof Sizes; // 'large' | 'normal' | 'small' | 'x'

// 反向获取对象属性的类型

type SizeValue = Sizes[keyof Sizes]; // string | number

// keyof any 可以理解为能作为“索引”的类型

type K = keyof any; // string | number | symbol

抽象的方法

抽象的方法实际上指的就是“函数”。我们来看看类型编程中,“函数”该怎么定义。

// “定义”

type Id<T> = T;

// “调用”

type A = Id<'a'>; // 'a'

// “参数”约束及默认值

type MakePair<T extends string, U extends number = 1> = [T, U];

type P1 = MakePair<'a', 2>; // ['a', 2]

type P2 = MakePair<'x'>; // ['x', 1]

看起来是不是和编程语言里面的函数很相似?这些“函数”的输入(参数)是类型,经过“运算”后,输出是“类型”。接着我们来看看在“函数体”(也就是等号右边)里面除了一些基本操作外,还可以做些其他什么骚操作。

“映射”操作(mapped)

将已有类型转换为一个新的类型,类似 map。返回的新类型一般是对象。

type MakeRecord<T extends keyof any, V> = {

[k in T]: V

};

type R1 = MakeRecord<1, number>; // { 1: number }

type R2 = MakeRecord<'a' | 1, string>; // { a: string, 1: string }

type TupleToObject<T extends readonly any[]> = {

[k in T[number]]: k

};

type O = TupleToObject<['a', 'b', 'c']>; // { a: 'a', b: 'b', c: 'c' }

条件——extends

条件类型可以理解为“三元运算”,T extends U ? X : Y,extends 可以类比为“相等”。

// 只保留string

type OnlyString<T> = T extends string ? T : never;

type S = OnlyString<1 | 2 | true | 'a' | 'b'>; // 'a' | 'b'

// 这里的计算过程大致是:

// 1 | 2 | true | 'a' | 'b' -> never | never | never | 'a' | 'b'

// 根据 x | never = x,最终得到 'a' | 'b'

// 获得对象的函数类型的属性key值

type FunctionPropertyNames<T> = {

[k in keyof T]: T[k] extends Function ? k : never

}[keyof T];

interface D {

id: number;

add(id: number): void;

remove(id: number): void;

}

type P = FunctionPropertyNames<D>; // 'add' | 'remove'

// 这里的计算过程大致是:

// 将 interface 展开:

// {

// id: D['id'] extends Function ? 'id' : never, //-> false

// add: D['add'] extends Function ? 'add' : never, //-> true

// remove: D['remove'] extends Function ? 'remove' : never //-> true

// }['id' | 'add' | 'remove']

// 计算条件类型:

// {

// id: never,

//. add: 'add',

// remove: 'remove'

// }['id' | 'add' | 'remove']

// 根据索引取值:

// never | 'add' | 'remove'

// 根据 never | x = x,最终得到:'add' | 'remove'

“析构“——infer

infer 可以理解为一种“放大镜”机制,可以“捕获”到被“嵌”在各种复杂结构里的类型信息。

// 对象 infer,可以取得对象某个属性值的类型

type ObjectInfer<O> = O extends { x: infer T } ? T : never;

type T1 = ObjectInfer<{x: number}>; // number

// 数组 infer,可以取得数组元素的类型

type ArrayInfer<A> = A extends (infer U)[] ? U : never;

const arr = [1, 'a', true];

type T2 = ArrayInfer<typeof arr>; // number | string | boolean

// tuple infer

type TupleInfer<T> = T extends [infer A, ...(infer B)[]] ? [A, B] : never;

type T3 = TupleInfer<[string, number, boolean]>; // [string, number | boolean]

// 函数 infer,可以取得函数的参数和返回值类型

type FunctionInfer<F> = F extends (...args: infer A) => infer R ? [A, R] : never;

type T4 = FunctionInfer<(a: number, b: string) => boolean>; // [[a: number, b: string], boolean]

// 更多其他的 infer

type PInfer<P> = P extends Promise<infer G> ? G : never;

const p = new Promise<number>(() => {});

type T5 = PInfer<typeof p>; // number

可以发现上面的例子需要使用 infer,是因为我们在“定义”时不知道具体的类型,需要在“调用”时做“推断”。infer 帮我们标注了待推断的类型,最终计算出实际的类型。

嵌套&递归

在“函数体”中,我们其实可以再“调用函数“,形成一种嵌套和递归的机制。

// 取函数第一个参数的类型

type Params<F> = F extends (...args: infer A) => any ? A : never;

type Head<T extends any[]> = T extends [any, ...any[]] ? T[0] : never;

type FirstParam = Head<Params<(name: string, age: number) => void>>; // string

// 递归定义

type List<T> = {

value: T,

next: List<T>

} | null;

尾声

文章写到这里基本就结束了,这篇文章的内容可能在平常的开发中会比较少遇到,但是对于补全自己的 TypeScript 体系、开阔视野还是有所帮助的。如果想更多的来些实战演练,推荐看看这个: https://github.com/type-chall...


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK