众所周知,TypeScript的类型系统因其高度灵活性而常常被戏称“类型体操”。各路高人纷纷在类型系统上卷了起来,实现了各种不可思议的功能。
最近徐飞叔叔还写了个中国象棋,可以说很卷了。zhuanlan.zhihu.com/p/426966480
其实复杂类型操作并非无迹可寻,本文就试图从元编程的角度挖掘一下类型系统的潜力,希望能够帮助你抓到一些思路和脉络。
元编程的基础是图灵完备的子系统,那么TypeScript类型系统是否是图灵完备的呢?答案当然是肯定的。
TypeScript类型系统的extends ?
构成了分支的能力,而允许递归,则形成了循环的能力,加上类型依赖本身可以形成顺序结构,满足了图灵完备的要求。
TypeScript的基础类型包括Number、Boolean、String、Tuple(元组)等,复杂类型则有函数、对象,尽管理论上获得了图灵完备,但我们仍需要一些基础的运算支撑。
元组操作
元组操作的核心是...
运算和infer
类型推断,...
可以把元组展开用于构造新的元组,而infer
允许我们从元组中分段匹配,并且获取其中各个部分。
type concat<A extends any[], B extends any[]> = [...A, ...B];
type shift<Tuple extends any[]> = Tuple extends [infer fist, ... infer rest] ? rest : void;
type unshift<Tuple extends any[], Type> = [Type, ...Tuple];
type pop<Tuple> = Tuple extends [... infer rest, infer last] ? rest : void;
type push<Tuple extends any[], Type> = [...Tuple, Type];
复制代码
当然了,其实这几个方法并没有存在的意义,实际使用中,我们并不需要这样的抽象,直接写右边的表达式即可。这里我们只是作为一个简单的热身运动,熟悉元组的特征。
...
和infer
在元组操作中的地位几乎是等同于加减法,是后续一切复杂运算的基础。
递归
递归是一切复杂类型操作的基石,在缺少减法和比较运算情况下,我们只能利用元组的长度和extends来实现比较,以下代码形成了一个定长列表:
代码语言:javascript复制type List<Type, n extends number, result extends any[] = []> =
result['length'] extends n ? result : List<Type, n, [...result, Type]>;
复制代码
一个复杂一点的例子,从名字就可以看出来是干什么用的:
代码语言:javascript复制type slice<Tuple extends any[], begin extends number, end extends number, before extends any[] = [], result extends any[] = []> =
before['length'] extends begin ?
[...before, ...result]['length'] extends end ?
result :
Tuple extends [...before, ...result, infer first, ...infer rest] ?
slice<Tuple, begin, end, before, [...result, first]> :
void :
Tuple extends [...before, infer first, ...infer rest] ?
slice<Tuple, begin, end, [...before, first], result> :
void ;
复制代码
字符串相关操作
字符串类型与元组类似,都是类型系统中的“一等公民”,通过模板匹配,我们可以截取字符串的各种部分,以下是几个示例,函数名都是大家熟悉的内容,此处不多解释了。
代码语言:javascript复制type numberToString<T extends number> = `${T}`;
type stringToChars<T extends string> = T extends `${infer char}${infer rest}` ? [char, ...stringToChars<rest>] : [];
type join<T extends (string|number|boolean|bigint|undefined|null)[], joiner extends string> =
T['length'] extends 1 ? `${T[0]}` :
T extends [infer first, ...infer rest] ? `${first}${joiner}${join<rest, joiner>}` :
''
复制代码
代码风格
因为没有语句,只能用extends ? :
结构,要想写出结构清晰的代码变得异常困难,这里我推荐一下我自己喜欢用的一种缩进风格。
规则1:串行结构
尽量让嵌套的extends ? :
出现在false分支中,这样,我们可以用一个问号对应一个结果,所有的extends不缩进,这个结构类似switch case,如:
type decimalDigitToNumber<n extends string> =
n extends '1' ? 1 :
n extends '2' ? 2 :
n extends '3' ? 3 :
n extends '4' ? 4 :
n extends '5' ? 5 :
n extends '6' ? 5 :
n extends '7' ? 7 :
n extends '8' ? 8 :
n extends '9' ? 9 :
n extends '0' ? 0 :
never
复制代码
规则2:合并条件
当extends ? :
出现在true分支中,如果需要,可以合并行,应当始终保持本行的 ? 和 : 数相等,并且以冒号结尾,如:
type and<v1 extends boolean, v2 extends boolean> =
v1 extends true ? v2 extends true ? true : false :
false
复制代码
规则3:嵌套结构
当extends ? :
出现在true分支中,可以在问号后断行,并让下一行缩进
type or<v1 extends boolean, v2 extends boolean> =
v1 extends false ?
v2 extends true ? true : false :
true
复制代码
number操作
TypeScript中虽然支持常量,但是它本身算不上友好,在类型系统中它几乎无法进行任何运算,本身的加减法都是没有的,但是我们可以把number转为数组做一些简单的运算:
代码语言:javascript复制type add<x extends number, y extends number> = [...List<any, x>, ...List<any, y>]['length'];
type minus<x extends number, y extends number> = List<any, x> extends [...rest, ...List<any, y>] ? rest['length'] : void;
type multiple<x extends number, y extends number, result extends any[] = [], i extends any[] =
[]> = i extends y ? result['length'] : multiple<x, y, [...result, List<x>], [...i, any]>;
复制代码
利用元组的['length']
来模拟number的各种运算,可以满足数字不大的情况下的一些运算需求,如果要进行大数运算,则需要通过前面的numberToString先转为字符串。可以参考字节前端此文 zhuanlan.zhihu.com/p/423175613
使用二进制表示整数
一种比较科学合理的做法是使用二进制来表示整数,它可以用来做一些较大规模的计算
代码语言:javascript复制type fromBinary<bin extends (0 | 1)[], i extends any[] = [], v extends any[] = [0] , r extends any[] = []> =
i['length'] extends bin['length'] ? r['length'] :
fromBinary<bin, [...i, 0], [...v, ...v], bin[i['length']] extends 0 ? r : [...r, ...v]>
复制代码
代码语言:javascript复制type not<bit extends (0|1)> = bit extends 0 ? 1 : 0;
type binaryAdd<bin1 extends (0 | 1)[], bin2 extends (0 | 1)[],
i extends any[] = [], extra extends (0 | 1) = 0, r extends (0|1)[] = []> =
i['length'] extends bin1['length'] ? r :
bin1[i['length']] extends 1 ?
bin2[i['length']] extends 1 ?
[extra, ...binaryAdd<bin1, bin2, [...i, 0], 1>] :
[not<extra>, ...binaryAdd<bin1, bin2, [...i, 0], extra>] :
bin2[i['length']] extends 1 ?
[not<extra>, ...binaryAdd<bin1, bin2, [...i, 0], extra>] :
[extra, ...binaryAdd<bin1, bin2, [...i, 0], 0>]
let g:fromBinary<binaryAdd<[...count<10, 0>, 1], [...count<9, 0>, 1, 0]>>;
复制代码
小试牛刀
好了,上面的例子都比较简单,似乎缺少一些元编程的味道,下面就让我们来挑战一下:编写一个TicTacToe的AI。
代码语言:javascript复制type not<b extends boolean> = b extends true ? false : true;
type Pattern = [
(' '|'⭘'|'✖'),(' '|'⭘'|'✖'),(' '|'⭘'|'✖'),
(' '|'⭘'|'✖'),(' '|'⭘'|'✖'),(' '|'⭘'|'✖'),
(' '|'⭘'|'✖'),(' '|'⭘'|'✖'),(' '|'⭘'|'✖')];
type toggleColor<color extends ('⭘'|'✖')> = color extends '⭘' ? '✖' : '⭘';
type checkline<v1, v2 , v3, color extends ('⭘'|'✖')> =
v1 extends color ? v2 extends color ? v3 extends color ? true : false : false : false;
type move<pattern extends Pattern, pos extends number, color extends ('⭘'|'✖'), _result extends (' '|'⭘'|'✖')[] = []> =
_result['length'] extends pattern['length'] ? _result :
_result['length'] extends pos ? move<pattern, pos, color, [..._result, color]> :
move<pattern, pos, color, [..._result, pattern[_result['length']]]>;
type isWinner<pattern extends Pattern, color extends ('⭘'|'✖')> =
checkline<pattern[0], pattern[1], pattern[2], color> extends true ? true :
checkline<pattern[3], pattern[4], pattern[5], color> extends true ? true :
checkline<pattern[6], pattern[7], pattern[8], color> extends true ? true :
checkline<pattern[0], pattern[3], pattern[6], color> extends true ? true :
checkline<pattern[1], pattern[4], pattern[7], color> extends true ? true :
checkline<pattern[2], pattern[5], pattern[8], color> extends true ? true :
checkline<pattern[0], pattern[4], pattern[8], color> extends true ? true :
checkline<pattern[2], pattern[4], pattern[6], color> extends true ? true :
false;
type emptyPoints<pattern extends Pattern, _startPoint extends any[] = [], _result extends any[] = []> =
_startPoint['length'] extends pattern['length'] ? _result :
pattern[_startPoint['length']] extends ' ' ? emptyPoints<pattern, [..._startPoint, any], [..._result, _startPoint['length']]> :
emptyPoints<pattern, [..._startPoint, any], [..._result]>;
type canWin<pattern extends Pattern, color extends ('⭘'|'✖'), _points extends any[] = emptyPoints<pattern>, _unchecked extends any[] = emptyPoints<pattern>, canDraw extends boolean = false> =
isWinner<pattern, toggleColor<color>> extends true ? "loose" :
_points['length'] extends 0 ? "draw" :
_unchecked['length'] extends 0 ? canDraw extends true ? "draw" : "loose" :
_unchecked extends [infer first, ...infer rest] ?
canWin<move<pattern, first, color>, toggleColor<color>> extends "loose" ? "win" :
canWin<move<pattern, first, color>, toggleColor<color>> extends "draw" ? canWin<pattern, color, _points, rest, true> :
canWin<move<pattern, first, color>, toggleColor<color>> extends "win" ? canWin<pattern, color, _points, rest, canDraw> :
`error1:${canWin<move<pattern, first, color>, toggleColor<color>>}` :
"error2";
type computerMove<pattern extends Pattern, color extends ('⭘'|'✖'), _points extends any[] = emptyPoints<pattern>, _unchecked extends any[] = emptyPoints<pattern>, canDraw extends boolean = false, bestPos = -1> =
checkOpenings<pattern, color> extends [true, infer pos] ? pos :
_unchecked['length'] extends 0 ? bestPos extends -1 ? _points[0] : bestPos :
_unchecked extends [infer first, ...infer rest] ?
canWin<move<pattern, first, color>, toggleColor<color>> extends "loose" ? first :
canWin<move<pattern, first, color>, toggleColor<color>> extends "draw" ? computerMove<pattern, color, _points, rest, true, first> :
canWin<move<pattern, first, color>, toggleColor<color>> extends "win" ? computerMove<pattern, color, _points, rest, canDraw, bestPos> :
`error1:${canWin<move<pattern, first, color>, toggleColor<color>>}` :
"error2";
type checkOpenings<pattern extends Pattern, color extends ('⭘'|'✖')> =
pattern extends [' ',' ',' ',' ',' ',' ',' ',' ',' '] ? [true, 4] : [false, never]
class Game<pattern extends Pattern, color extends ('⭘'|'✖')>{
board:{
line1: `${pattern[0]}${pattern[1]}${pattern[2]}`
line2: `${pattern[3]}${pattern[4]}${pattern[5]}`
line3: `${pattern[6]}${pattern[7]}${pattern[8]}`
canWin:canWin<pattern, color>
emptyPoints:emptyPoints<pattern>
color:color
computer:computerMove<pattern, color>
},
move<pos extends (0|1|2|3|4|5|6|7|8)>(p:pos) {
return new Game<move<pattern, pos, color>, toggleColor<color>>()
}
}
let c:Game<[' ',' ',' ',' ',' ',' ',' ',' ',' '],'⭘'>;
c.move(4).move(1).board
复制代码
www.typescriptlang.org/play?ts=4.5…
好了,如果你看到这里,相信对TypeScript类型元编程已经有了初步的了解,接下来可以把它灵活运用到日常工作中啦。