TypeScript类型元编程入门指南

·  阅读 2796

众所周知,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来实现比较,以下代码形成了一个定长列表:


type List<Type, n extends number, result extends any[] = []> = 
    result['length'] extends n ? result : List<Type, n, [...result, Type]>;

复制代码

一个复杂一点的例子,从名字就可以看出来是干什么用的:

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 ;

复制代码

字符串相关操作

字符串类型与元组类似,都是类型系统中的“一等公民”,通过模板匹配,我们可以截取字符串的各种部分,以下是几个示例,函数名都是大家熟悉的内容,此处不多解释了。


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转为数组做一些简单的运算:

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

使用二进制表示整数

一种比较科学合理的做法是使用二进制来表示整数,它可以用来做一些较大规模的计算


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]>

复制代码
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。


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…

image.png

好了,如果你看到这里,相信对TypeScript类型元编程已经有了初步的了解,接下来可以把它灵活运用到日常工作中啦(被同事打了请不要说出我)。

分类:
前端
标签:
分类:
前端
标签: