Typescript类型编程奇奇怪怪的写法对于刚接触的朋友比较陌生,这份资料目的就是按编程脉络,将Typescript类型编程的写法罗列出来,方便编写时查询。
在理解完基础逻辑后,推荐配合type-challenges的部分题目来熟悉语法,TypeHero 也是一个很不错的选择。由于只谈类型编程,阅读时需要对Typescript有一定的了解。
所有的示例可以黏贴到这里做测试:https://www.typescriptlang.org/zh/play
类型
既然是类型编程,那么类型就是最基础的元素,先来回忆一下。
类型定义
原始类型:
boolean
string
number
bigint
symbol
复合类型:
object
、Object
:对象类型。Object
类型代表广义的对象,即JavaScript中所有可以被视为对象的值。可以使用{}
替代表示。object
类型代表比较精确的对象,即对象、数组、函数。
Array
、[]
:数组或者元组- 数组,所有成员类型都相同,且个数不固定
- 元组,所有成员类型可以不相同,且个数固定
enum
:枚举类型
特殊类型:
void
:没返回值undefined
:受strictNullChecks
参数控制,可以开启赋值给任意类型null
:受strictNullChecks
参数控制,可以开启赋值给任意类型never
:空类型,无法接收任意类型,可以赋值给任意类型unknown
:可以接收任意类型,无法赋值给任意类型any
:可以赋值给任意类型,也能接收任意类型,即关闭类型检查
值类型:
- 变量可以配置具体的某个值当做类型
其他构造函数:
Function
Error
Date
- ...等等
联合与交叉类型
|
:联合类型,可以将原始类型、复合类型、特殊类型或值类型组合成一个联合类型&
:交叉类型,可以将原始类型、复合类型组合成一个交叉类型
类型属性符
?
:可选属性readonly
:只读属性。例如:type n = readonly number
、type n = Readonly<number>
as const
:效果与只读属性一样,可以在末尾直接添加as const
用来快速配置只读属性。例如const arr = [1, 2, 3] as const;
unique
:Symbol 类型使用unique symbol
来表达具体的Symbol实例。
类型运算符
除了类型,Typescript还提供了一些必要的逻辑运算关键字:
typeof
:返回运算目标的类型keyof
:返回运算目标所有的keyin
:检查指定类型是否在目标类型中[]
:取出对象的指定键名的类型值extends ? true : false
:条件运算,类似于三元运算符infer
:定义新的类型参数,需要在extends运算符一起使用is
:约束类型`
`:字符串模板,其中可以使用${}
来引用 string、number、bigint、boolean、null、undefined这六种类型,也可以做字符串组合与匹配。...
:扩展运算符。用来处理不确定的剩余数据,标注类型只能是数组或元组+
:可以给类型添加某个属性-
:可以给类型去除某个属性as
、<>
:类型断言。可以将一种类型断言成其他类型satisfies
:检查某个值是否符合指定类型
对于用过Typescript的朋友可能会有点印象,但是到类型编程里需要怎样书写会比较懵,在接下来的案例中我们将用到这些运算符。
类型编程逻辑元素
既然是类型编程,我们就按平常编程归纳一下步骤:一般情况下会先创建变量,然后对变量进行一定的逻辑运算,最后返回值。逻辑运算中可能会需要做一些基础类型判断、分支语句逻辑与遍历逻辑。
变量是什么?变量就是我们需要处理的对象。Typescript没有提供let const之类类型关键字,如何定义变量呢?Typescript没办法回车换行,在处理过程中又需要怎么创建新的处理对象?类型参数一节会罗列这块写法。
逻辑处理就是顺序执行、条件分支以及数据循环,顺序执行不用多说,在谈论完类型参数后会罗列条件分支、数据循环的写法。
这两块熟悉完后对于类型编程已经能上手了,之后就是列举一些常见的处理场景,来熟练巩固语法的写法。
内置类型工具
Typescript有内置一部分类型工具,可以简化部分处理逻辑:链接。
类型参数
类型编程里因为创建的符号实际上不能重新赋值,所以我们称之为类型参数。创建有两种形式:
直接在尖括号里定义参数
type GetType<T> = T; type ret = GetType<string>; // type ret = string
赋予默认值
type GetTypeNumber<T = number> = T; type ret = GetTypeNumber; // type ret = number
约束类型
type GetArgStringOrNumber<T extends string | number> = T; type ret = GetArgStringOrNumber<bigint>; // Type 'bigint' does not satisfy the constraint 'string | number'.(2344)
使用
infer
关键字创建infer
关键字大部分情况下需要接在extends
关键字之后。比如,可以获取Promise中值的类型
type GetType<T> = T extends Promise<infer V> ? V : T; type ret = GetType<Promise<{}>>; // type ret = {}
可以利用字符串模板的匹配能力提取部分字符串,比如提取首字母
type GetFirst<T> = T extends `${infer first}${string}` ? first : T; type ret = GetFirst<'First'>; // type ret = "F"
可以提取数组指定位的信息
type GetLast<T> = T extends [...infer _, infer last] ? last : T; type ret = GetLast<[1, 2, 3]>; // type ret = 3
在函数定义上可以单独使用infer关键字
比如剩余参数的类型集合
type GetParameters<F extends Function> = F extends ( ...args: infer Args ) => unknown ? Args : never; type ret = GetParameters<(a1: string, a2: string) => string>; // type ret = [a1: string, a2: string]
或者返回值的推断
type MyReturnType<T> = T extends (...argv:any[]) => infer T ? T :never; type ret = MyReturnType<()=> Promise<{ code: 1 }>>; // type ret = Promise<{ code: 1; }>
类型收缩与转换
as
as
可以直接将类型断言为另外无关的类型,很常见就不多举例了。extends
extends
可以对类型进行约束收缩,避免出现类型报错。以上已经有应用示例。&
交叉类型运算可以让类型参数中符合条件的保留下来(做交集)。
type ret1 = ('str1' | 'str2' | 123) & string; // type ret1 = "str1" | "str2" type ret2 = ('str1' | 'str2' | 123) & (string | number); // type ret1 = "str1" | "str2" | 123
条件分支
有了参数之后我们往往需要对数据做判断,这就涉及到条件分支语句。
在Typescript中没有if else语句,但有个类似三元运算符的 extends ? :
语法(上面infer语法举例已经用到了),我们用此来替代条件分支。
extends
用到的场景有点多,单独使用时是继承,继承限制了范围,所以也可以拿来做类型约束。在之后接上? :
就变成了条件分支。在这之间可以用infer关键字来创建新的类型参数。
比如我们要判断一个类型是否为string,首先使用extends
约束类型,后面接上? :
,当为true、false时返回我们所需要的内容。
type IsString<T> = T extends string ? true : false;
type ret = IsString<'hello'>; // type ret = true
判断数据为数组
type IsArray<T> = T extends unknown[] ? true : false;
type ret = IsArray<[]>; // type ret = true
多层if判断,判断类型是否为string或是number
type IsStringOrNumber<T> = T extends string ? true
: T extends number ? true
: false;
type ret1 = IsStringOrNumber<'hello'>; // type ret1 = true
type ret2 = IsStringOrNumber<987>; // type ret2 = true
循环
循环有多种场景,我们分开讨论。
使用关键字
keyof
取对象的所有Key
keyof最基础的用法,遍历出目标对象的所有Key
type GetKey<T> = keyof T; type ret = GetKey<{ name: string; age: number; }>; // type ret = keyof Person
获取对象的所有value类型
type GetValue<T> = T[keyof T]; type ret = GetValue<{ name: string; age: number; }>; // type ret = string | number
根据传入的对象动态推导出返回值类型
配合extends 约束K的类型是T的所有Key。
// 根据给定对象约束传入的key 推导出对应的返回值类型 type Prop<T extends object, K extends keyof T> = T[K];
对象的key增加或删除属性
可以增加或者删减属性字段配置。
// 增加只读属性 TS内置了Readonly工具 type MyReadonly<T> = { readonly [K in keyof T]: T[K] } type ret = MyReadonly<{ name: string; age: number; }>; // type ret = { readonly name: string; readonly age: number; } // 删除可选属性 TS内置了Required工具 type MyRequired<T> = { [P in keyof T]-?: T[P] } type ret2 = MyRequired<{ name?: string; age?: number; }>; // type ret2 = { readonly name: string; readonly age: number; }
对象key进行二次处理
对key处理使用
as重映射
语法。这是将Key转换为首字母大写的示例,
Capitalize
是内置的首字母大写类型工具。type CapitalizeKey<T extends object> = { [K in keyof T as K extends string ? Capitalize<K> : K ]: T[K]; }; type ret = CapitalizeKey<{ one: 1; two: 2; 3: 3 }>; // type ret = { One: 1; Two: 2; 3: 3; }
递归
其他需要重复处理的形式可以用递归解决。
递归就是在处理逻辑中调用自身,比如下面这个颠倒数组的例子,每次取出第一位的数据,然后放到重组后的数据的最后位。
type Reversal<T> = T extends [infer first, ...infer rset] ? [...Reversal<rset>, first] : T; type ret = Reversal<[1,2,3,4,5,6]>; // type ret = [6, 5, 4, 3, 2, 1]
另一个常用领域是处理字符串的时候,Typescript字符串模板匹配字符串的时候匹配到一个即停止,如果有多个符合条件的匹配就需要使用递归处理。(字符串模板配infer可以理解为单次的split,用已知字符去匹配目标字符串,匹配到了就分割,但只分割一次,重复的不再次分割)
举个例子,我们需要实现去除所有下划线,将剩余字符串调用自身再次处理,直到所有匹配的字符串都处理完为止。
type RemoveAllUnderline<T extends string> = T extends `_${infer str}` ? RemoveAllUnderline<str> : T; type ret = RemoveAllUnderline<'___Hello World'>; // type ret = "Hello World"
还有一个类似的例子就是全量替换,但代码看起来会复杂一些,逻辑是一样的。
核心依然是根据需要匹配的字符串拆分字符,这样我们就有了以匹配字符串为核心的左中右三组。中间字符串是需要替换的目标,只需要拿左侧、右侧与替换后的字符拼接就是目标字符串了。因为一次只处理一个字符,所以需要递归调用。这里因处理字符串是从左到右处理,所以左侧一定是处理过的了,只需要对右侧的字符串做递归即可。
type ReplaceAll<S extends string, From extends string, To extends string> = From extends '' ? S : S extends `${infer left}${From}${infer right}` ? `${left}${To}${ReplaceAll<right, From, To>}` : S; type ret = ReplaceAll<'~ ~ Hello World ~ ~', '~', '!'>; // type ret = "! ! Hello World ! !"
联合类型
联合类型比较特殊,Typescript处理时自动会将每个类型单独传入,所以不需要特别处理遍历。以下是一个替换例子,没用到循环与递归就能将联合类型中的元素单独处理。
type Union = 'sky' | 'lawn' | 'flower'; type Replace<T> = T extends 'flower' ? `bloom` : T; type ret = Replace<Union> // type ret = "sky" | "lawn" | "bloom"
类型处理场景列举
学完以上知识点我们就可以列举一些常见场景的组合用法了。这里可以配合type-challenges、TypeHero 题目训练加强理解。
字符串类型
Typescript中没有提供那么多工具函数,匹配、拆分、合并字符串全都依赖字符串模板工具。
字符串模板中有自动匹配机制,会根据已提供的字符串去拆分目标字符串。下面是一些示例:
合并
合并比较简单,直接使用
${}
语法来拼接数据type Merge<S extends string> = `Hello ${S}`; type ret = Merge<'World'>; // Hello World
匹配与拆分
字符串模板可以根据提供的字符或者类型,将字符串划分成多个部分。
这里为了创建新的类型参数使用到了
extends
,其实可以将这种视为一种固定匹配格式extends
${infer X}? X : never;
在extends 之后的字符串模板中创建新的类型参数。比如分割字符串,根据下划线分割,后面部分是只要符合字符串类型即可匹配成功
type Spilt<S extends string> = S extends `${infer first}_${string}` ? first : never; type ret = Spilt<'Hello_World'>; // Hello
如何后面改成number类型,传入的只能是数字(也不能有小数点),否则匹配不到。
type Spilt<S extends string> = S extends `${infer first}_${number}` ? first : never; type ret = Spilt<'Hello_123'>; // Hello
字符串匹配只会匹配遇到的第一个字符,后续如果有相同字符也不会再匹配。
type RemoveUnderline<S extends string> = S extends `_${infer str}` ? str : S; type ret = RemoveUnderline<'___Hello World'>; // type ret = "__Hello World"
要连续处理的话得需要用递归的逻辑。
type RemoveAllUnderline<S extends string> = S extends `_${infer str}` ? RemoveAllUnderline<str> : S; type ret = RemoveAllUnderline<'___Hello World'>; // type ret = "Hello World"
如果要处理右侧的字符需要另外重新写匹配逻辑,这场景最经典的就是处理前后空字符串问题
type Trim<S extends string> = TrimLeft<TrimRight<S>>; type TrimLeft<S extends string> = S extends ` ${infer str}` ? TrimLeft<str> : S; type TrimRight<S extends string> = S extends `${infer str} ` ? TrimRight<str> : S; type ret = Trim<' Hello World '> // type ret = "Hello World"
转换为驼峰命名法
逻辑依然是拆分字符串,稍微不同的是这里用了内置工具函数
Uppercase
来实现首字母大写能力。type CamelCase<S extends string> = S extends `${infer left}_${infer right}${infer rest}` ? `${left}${Uppercase<right>}${CamelCase<rest>}` : S; type ret = CamelCase<'camel_case'>; // type ret = "camelCase"
转为蛇形命名法
如果需要短横命名法只需要更换一下横线即可
type SnakeCase<S extends string> = S extends `${infer char}${infer rest}` ? rest extends Uncapitalize<rest> ? `${Uncapitalize<char>}${SnakeCase<rest>}` : `${Uncapitalize<char>}_${SnakeCase<rest>}` : S; type ret = SnakeCase<"SnakeCase">; // type ret = "snake_case"
字符串转数组
逻辑与上面一样,先取出一个字符,然后利用递归将剩余的字符串重复处理,不断展开并拼接成新数组。
type StringToArray<S extends string> = S extends `${infer char}${infer rest}` ? [char, ...StringToArray<rest>] : []; type ret = StringToArray<"Hello World">; // type ret = ["H", "e", "l", "l", "o", " ", "W", "o", "r", "l", "d"]
数组、元组类型
数组匹配就是按位匹配,如果不确定是第几位就需要用...
运算符处理。
取数组第一位
type GetFirst<T> = T extends [infer first, ...infer _] ? first: T; type ret = GetFirst<[1, 2, 3]>; // type ret = 1
取数组最后一位
type GetLast<T> = T extends [...infer _, infer last] ? last : T; type ret = GetLast<[1, 2, 3]>; // type ret = 3
去除数组第一位
不需要的位可以使用
unknown
关键字占位type Unshift<T> = T extends [unknown, ...infer last] ? last : T; type ret = Unshift<[1, 2, 3]>; // type ret = [2, 3]
取数组指定位
type GetIndexValue<T, I extends keyof T> = T[I]; type ret = GetIndexValue<[1, 2, 3], 1>; // type ret = 2
修改创建新数组
将数组拆分后重组就是创建新数组,重组的时候省略某些值就是删除,在原来位写入新值就是替换,所有的基础逻辑都是数组的拆分。
以下是一个取数组最后一位并用传入的新值创建新数组示例
type CreateNewArr<T, U> = T extends [...infer _, infer last] ? [last, U] : T; type ret = CreateNewArr<[1, 2, 3], 4>; // type ret = [3, 4]
我们也可以使用
...
运算符来展开数组,比如合并两个数组类型数据type Concat<T extends unknown[], U extends unknown[]> = [...T,...U]; type ret = Concat<['1', 2], [3]>; // type ret = ["1", 2, 3]
数组的长度信息
需要获数组的数量时,同样是取length,只是写法没那么漂亮
type GetLenght<T extends unknown[]> = T['length']; type ret = GetLenght<[0, 1, 2, 3, 4]>; // type ret = 5
数组转字符串类型
同样将数组元素一一取出,然后用字符串模板重新拼接元素。
S变量是为了存储处理后的字符串,字符串模板只支持自动转换简单的类型,T是数组无法直接写成类似这样的语句
${arrayToString<rest>}
,所以借助变量来进行处理。type ExtendsType = string | number | boolean | undefined | null | bigint type ArrayToString< T extends unknown[], S extends string = "" > = T extends [infer first extends ExtendsType, ...infer rest extends ExtendsType[]] ? ArrayToString<rest, `${S}${first}`> : S; type ret = ArrayToString<[0, 1, '2', 'str', 99n, true, null]> // type ret = "012str99truenull"
对象类型
对象类型大部分就是对索引的处理。
根据传入类型约束key
// 根据给定对象约束传入的key 推导出对应的返回值类型 type Prop<T extends object, K extends keyof T> = T[K];
提取指定Key
type MyPick<T, K extends keyof T> = { [P in K]: T[P] }; type ret = MyPick<{ one: 1, two: 2 }, 'two'>; // type ret = { two: 2; }
剔除指定Key
Exclude
是内置工具,可以获得排除指定值的数据(差集)。Pick
是内置工具函数用于提取指定Keytype MyOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>; type ret = MyOmit<{ one: 1, two: 2 }, 'two'>; // type ret = { one: 1; }
重映射语法实现首字母大写
type CapitalizeKey<T extends object> = { [K in keyof T as K extends string ? Capitalize<K> : K ]: T[K]; }; type ret = CapitalizeKey<{ one: 1; two: 2; 3: 3 }>; // type ret = { One: 1; Two: 2; 3: 3; }
重新指定Value类型
type ObjectVaule<T extends object> = { [K in keyof T]: T[K] extends string ? `${T[K]}!` : [T[K]] } type ret = ObjectVaule<{ one: 1, two: 2, three: 'three' }>; // type ret = { one: [1]; two: [2]; three: "three!"; }
Value与Key位置翻转
利用重映射语法,将值写在Key的位置,将Key写在值的位置即可
type Flip<T extends Record<string, string | number | boolean>> = { [P in keyof T as `${T[P]}`]: P }; type ret = Flip<{ one: 1, two: 2 }>; // type ret = { 1: "one"; 2: "two"; }
联合类型
联合类型与其他类型不一样,会单独传入每一项进行匹配,所以处理的时候不需要特别写循环逻辑。
提取交集
type MyExtract<T, U> = T extends U ? T : never; type ret = MyExtract<'one' | 'two', 'two' | 'three'>; // type ret = "two"
提取差集
type MyExclude<T, K> = T extends K ? never : T; type ret = MyExclude<'one' | 'two' | 'three', 'three'>; // type ret = "one" | "two"
字符串转联合类型
type StringToUnion<T extends string> = T extends `${infer first}${infer rest}` ? first | StringToUnion<rest> : never; type ret = StringToUnion<"Hello"> // type ret = "H" | "e" | "l" | "o"
数组转联合类型
type TupleToUnion<T extends unknown[]> = T[number]; type ret = TupleToUnion<[1, 2, 'str']> // type ret = 1 | 2 | "str"
阻断联合类型处理
将泛型参数用
[]
括起来可以阻断联合类型处理。比如有一个类型判断工具函数,如果传入的是联合类型,每个值会有单独返回,这里一个值是true一个值是false,所以显示的是boolean。这种场景下我们需要将传入的类型视为一个整体,所以加上括号做类型阻断,这时候就能正常返回false了。
never类型在这里也比较特殊,被认为是没有联合类型,所以直接返回never。这种场景下也是用阻断联合类型来处理就能返回正确的值了。
type isString<T> = T extends string ? true : false; type ret = isString<'hello' | 123>; // type ret = boolean type ret2 = isString<never>; // type ret = never type isStringPlus<T> = [T] extends string ? true : false; type retPlus = isStringPlus<'hello' | 123>; // type retPlus = false type retPlus2 = isStringPlus<never>; // type retPlus2 = false
函数入参定义
提取入参参数
type GetParameters<F extends Function> = F extends ( ...args: infer Args ) => unknown ? Args : never; type ret = GetParameters<(a1: string, a2: string) => string>; // type ret = [a1: string, a2: string]
获取函数返回类型
type MyReturnType<T> = T extends (...argv:any[]) => infer T ? T :never; type ret = MyReturnType<()=> Promise<{ code: 1 }>>; // type ret = Promise<{ code: 1; }>
获取函数的This类型
Typescript中函数定义的第一个参数可以定义this类型,用这个特性我们可以获取this的信息。
type GetThisParameterType<T> = T extends ( this: infer type, ...args: never ) => any ? type : unknown; type ret2 = GetThisType< ( this: { text: string; print: () => string }, a1: string, a2: string ) => string >; // type ret = { text: string; print: () => string; }
基础类型工具定义
条件判断
boolean是联合类型,定义类型 type Bool = false | true;
if
type If<Cond extends Bool, Then, Else> = Cond extends true ? Then : Else;
not
type Not<A extends Bool> = A extends true ? false : true;
and
type And<A extends Bool, B extends Bool> = If<A, If<B, true, false>, false>;
or
type Or<A extends Bool, B extends Bool> = If<A, true, If<B, true, false>>;
类型判断
Any
type IsAny<T> = 0 extends (1 & T) ? true : false;
Never
type IsNever<T> = [T] extends [never] ? true : false;
Boolean
type IsBoolean<T> = T extends boolean ? true : false;
Number
type IsNumber<T> = T extends number ? true : false;
String
type IsString<T> = T extends string ? true : false;
Null
type IsNull<T> = T extends null ? true : false;
Undefined
type IsUndefined<T> = T extends undefined ? true : false;
Nil
type IsNil<T> = Or<IsNull<T>, IsUndefined<T>>;
Array
type IsArray<T> = T extends unknown[] ? true : false;
IsFunction
type IsFunction<T> = T extends Function ? true : false;
Object
type IsObject<T> = T extends object ? true : false; type IsRealObject<T> = And< T extends object ? true : false, And<Not<IsFunction<T>>, Not<IsArray<T>>> >;
IsUnion
type IsUnion<T, U = T> = T extends U ? ([U] extends [T] ? false : true) : never;
数学运算
构造指定个数数组
Typescript没提供数学运算能力,数学运算的实现都需要借助数组长度来处理。
type BuildArr< Length extends number, Item, Arr extends unknown[] = [] > = Arr["length"] extends Length ? Arr : BuildArr<Length, Item, [...Arr, Item]>; type ret = BuildArr<3, "!">; // type ret = ["!", "!", "!"]
加法
type Add<T extends number, S extends number> = [ ...BuildArr<T, number>, ...BuildArr<S, number> ]["length"]; type ret = Add<2, 3>; // type ret = 5
减法
type Subtract<T extends number, S extends number> = BuildArr< T, number > extends [...arr1: BuildArr<S, number>, ...arr2: infer Rest] ? Rest["length"] : never; type ret = Subtract<3, 2>; // type ret = 1
乘法
type Mutiply< T extends number, S extends number, ResultArr extends unknown[] = [] > = S extends 0 ? ResultArr["length"] : Mutiply<T, Subtract<S, 1>, [...BuildArr<T, number>, ...ResultArr]>; type ret = Mutiply<3, 2>; // type ret = 6
除法
type Divide< T extends number, S extends number, CountArr extends unknown[] = [] > = T extends 0 ? CountArr["length"] : Divide<Subtract<T, S>, S, [unknown, ...CountArr]>; type ret = Divide<6, 2>; // type ret = 3
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。