[FE] TypeScript 类型编程(小结)

1. TypeScript 基本类型

TypeScript 有以下这些基本类型:string, number, boolean
单个的值也是看做类型,1, 'a', null, true

类型可以看做是值的 “集合”。
Effective TypeScript: (Item 7)Think of Types as Sets of Values

2. 构造新类型

2.1 类型运算

类型之间可以组合成新的类型,

  • 交集:&
  • 并集:|
  • 类型构造器:数组 [], interface {}, 新的字符串类型 ${a}${b}

2.2 类型函数

可以借用泛型实现类型上的函数,进行类型变换,把入参类型,转换成出参类型,

type func<x extends string, y extends string> = {  // 如果只进行类型编程,参数的大小写就无所谓了
  result: [x, y, `${x}-${y}`]
};

a extends b 用来判断 类型 a 是否 b 的子类型(子集),

  • 用于 类型函数的参数中,表示对类型参数进行限制
  • 用于 类型函数体中,表示分支判断(下文介绍)

可以用 ts-toolbelt 跑一下测试,

import { Test } from 'ts-toolbelt';
const { checks, check } = Test;

checks([
  check<  // 判断两个类型是否相等
    func<'a', 'b'>,
    {
      result: ['a', 'b', `a-b`]
    },
    Test.Pass
  >(),
]);

值得一提是,类型相等性判断不同人可能会有不同做法,ts-toolbelt/Equals 中用的是,

type Equals<A1 extends any, A2 extends any> = 
  (<A>() => A extends A2 ? 1 : 0) extends (<A>() => A extends A1 ? 1 : 0) 
  ? 1 
  : 0;

参考 github issue: type level equal operator

3. 类型编程

我们知道编程语言的控制结构包括三种:顺序、选择、循环。

  • 顺序:使用 类型函数(上文介绍了)
  • 选择:使用 extends
  • 循环:使用 类型函数 的递归

3.1 分支判断

在类型函数中,可以使用 extends 来实现分支判断(使用 infer 可实现模式匹配)。

例如,

type func<str> = 
  str extends `${infer first}${infer tail}`
  ? [first, tail]
  : unknown
import { Test } from 'ts-toolbelt';
const { checks, check } = Test;

checks([
  check<  // 判断两个类型是否相等
    func<'abc'>,
    ['a', 'bc'],
    Test.Pass
  >(),
]);

3.1 循环(递归)

type array<head extends string, tail extends string[]>
  = [head, ...tail];

type join<strs extends string[], result extends string>
  = strs extends [] ? result
  : strs extends array<infer head, []> ? `${result}${head}`
  : strs extends array<infer head, infer tail> ? join<tail, `${result}${head}`>
  : never;
import { Test } from 'ts-toolbelt';
const { checks, check } = Test;

checks([
  check<
    join<['hello', ' ', 'world'], ''>,
    'hello world',
    Test.Pass
  >(),
]);

其中引入了辅助函数 array,是为了限定 headtail 的类型。
否则会报以下错误,

(1)`${result}${head}`
Type 'head' is not assignable to type 'string | number | bigint | boolean'.
  Type 'head' is not assignable to type 'number'.ts(2322)

(2)join<tail, `${result}${head}`>
Type 'tail' does not satisfy the constraint 'string[]'.
  Type 'unknown[]' is not assignable to type 'string[]'.
    Type 'unknown' is not assignable to type 'string'.ts(2344)
Type 'head' is not assignable to type 'string | number | bigint | boolean'.
  Type 'head' is not assignable to type 'number'.ts(2322)

出错的示例如下,

type join<strs extends string[], result extends string>
  = strs extends [] ? result
  : strs extends [infer head] ? `${result}${head}`
  : strs extends [infer head, ...infer tail] ? join<tail, `${result}${head}`>
  : never;

4. keyofin

在学习 Mapped Types 的时候,
经常会看到 keyofin 两个操作符,曾经造成过一些困扰,这里总结如下。

例子,

type func<input> = {
  [props in keyof input]: {  // 这里 keyof input 为:'a' | 'b'
    [field in props]: input[field]  // props 分别为 'a'(可以看做只有一个类型的 union) 和 'b'
  }
}
import { Test } from 'ts-toolbelt';
const { checks, check } = Test;

checks([
  check<
    func<{ a: number, b: string }>,
    { a: { a: number }, b: { b: string } },
    Test.Pass
  >(),
]);

这里有几个值得注意的点:

(1)通过 keyof 获取属性类型的 union

type func<obj> = keyof obj;  // 所有属性名构成的 union
import { Test } from 'ts-toolbelt';
const { checks, check } = Test;

checks([
  check<
    func<{ a: number, b: string }>,
    'a' | 'b',
    Test.Pass
  >(),
]);

(2)值类型的 union

type func<obj, k extends keyof obj> = obj[k];  // 获取属性值对应值的 union【k 是一个 union】
import { Test } from 'ts-toolbelt';
const { checks, check } = Test;

checks([
  check<
    func<{ a: number, b: string, c: boolean }, 'a' | 'c'>,
    number | boolean,
    Test.Pass
  >(),
]);

(3)使用 in 进行循环操作

type func<obj, k extends keyof obj> = {
  [prop in k]: obj[prop]
};
import { Test } from 'ts-toolbelt';
const { checks, check } = Test;

checks([
  check<
    func<{ a: number, b: string, c: boolean }, 'a' | 'c'>,
    { a: number, c: boolean },
    Test.Pass
  >(),
]);

5. 内置函数

TypeScript 内置了一些 类型函数,称为 Utility Types
位于 typescript/lib/lib.es5.d.ts L1471-L1561

本地位置通常在这里,

/Applications/Visual Studio Code.app/Contents/Resources/app/extensions/node_modules/typescript/lib/lib.es5.d.ts

我们来学习一下这些内置函数的实现,

// 所有字段变成可选:{a: number, b?:string} -> {a?:number, b?:string}
// 已经可选的不受影响
type Partial<T> = {
    [P in keyof T]?: T[P];
};

// 所有字段变成必填:{a?: number, b:string} -> {a:number, b:string}
// 已经必填的不受影响
type Required<T> = {
    [P in keyof T]-?: T[P];
};

// 所有字段变成 readonly
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

// 过滤 interface T,只留下给定 prop
// Pick<{a:1,b:2,c:3}, 'a'|'c'> -> {a:1,c:3}
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

// 这里必须限定 K 是 `extends keyof any`,否则会报错
// Type 'K' is not assignable to type 'string | number | symbol'.
//  Type 'K' is not assignable to type 'symbol'.ts(2322)
// Record<'a'|'b', 1> -> {a:1,b:1}
type Record<K extends keyof any, T> = {
    [P in K]: T;
};

// 计算差集 T-U
// 判断 T 中(union)的每一个部分,是否是 U 的子类型,是就去掉,否则留下,最后将结果 union 起来
// Exclude<'a'|'b', 'b'|'c'> -> 'a'
type Exclude<T, U> = T extends U ? never : T;

// 计算 交集
// Extract<'a'|'b', 'b'|'c'> -> 'b'
type Extract<T, U> = T extends U ? T : never;

// 从 T 中去掉部分 props
// Omit<{a:1,b:2,c:3}, 'a'|'c'> -> {b:2}
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

// 约束 T 不能是 null 或 undefined
type NonNullable<T> = T extends null | undefined ? never : T;

// 通过模式匹配 infer,获得函数的参数类型
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

// 获取 constructor 的参数类型
// abstract 指的是 https://www.tutorialsteacher.com/typescript/abstract-class
type ConstructorParameters<T extends abstract new (...args: any) => any> = T extends abstract new (...args: infer P) => any ? P : never;

// 获取函数的返回值类型
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

// 获取构造函数示例的类型
type InstanceType<T extends abstract new (...args: any) => any> = T extends abstract new (...args: any) => infer R ? R : any;

// 将 S 转换成大写(intrinsic 表示需要 TypeScript 内部来实现)
type Uppercase<S extends string> = intrinsic;
// 将 S 转换成小写
type Lowercase<S extends string> = intrinsic;
// 将 S 转换成 首字母大写
type Capitalize<S extends string> = intrinsic;
// 将 S 转换成首字母小写
type Uncapitalize<S extends string> = intrinsic;

6. 加法运算

type L<n extends number, r extends never[]>
  = r['length'] extends n ? r
  : L<n, [never, ...r]>

type add<x extends number, y extends number>
  = [...L<x, []>, ...L<y, []>]['length'];

type minus<x extends number, y extends number>
  = L<x, []> extends [...head: L<y, []>, ...tail: infer z] ? z['length']
  : never;

type mul<x extends number, y extends number>
  = y extends 0 ? 0
  : mul<x, minus<y, 1>> extends infer r
    ? r extends number ? add<r, x>  // 手工限定递归步骤的类型为 number
    : never
  : never;
import { Test } from 'ts-toolbelt';
const { checks, check } = Test;

checks([
  check<
    mul<2, 3>,  // 实现类型上的乘法
    6,
    Test.Pass
  >(),
]);

值得一提的是 mul 的实现,以下实现方式会报错,

type mul<x extends number, y extends number>
  = y extends 0 ? 0
  : add<mul<x, minus<y, 1>>, x>;  // Type instantiation is excessively deep and possibly infinite. ts(2589)

解决方案 参考 Type instantiation is excessively deep and possibly infinite. ts(2589)


参考

TypeScript Type-Level Programming
用 TypeScript 模板字面类型来制作 URL parser
用 TypeScript 类型运算实现一个中国象棋程序
TypeScript 类型体操天花板,用类型运算写一个 Lisp 解释器

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 228,363评论 6 532
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 98,497评论 3 416
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 176,305评论 0 374
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 62,962评论 1 311
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 71,727评论 6 410
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 55,193评论 1 324
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 43,257评论 3 441
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 42,411评论 0 288
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 48,945评论 1 335
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 40,777评论 3 354
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 42,978评论 1 369
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 38,519评论 5 359
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 44,216评论 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 34,642评论 0 26
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 35,878评论 1 286
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 51,657评论 3 391
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 47,960评论 2 373

推荐阅读更多精彩内容