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
,是为了限定 head
和 tail
的类型。
否则会报以下错误,
(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. keyof
和 in
在学习 Mapped Types 的时候,
经常会看到 keyof
和 in
两个操作符,曾经造成过一些困扰,这里总结如下。
例子,
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 解释器
网友评论