最近 Hejlsberg 提交了一个 PR 让 TS 支持了 Template Type:
https://github.com/microsoft/TypeScript/pull/40336
https://github.com/microsoft/TypeScript/pull/40336
这个特性目前还没正式发 npm 包,但可以在: TypeScript Playground 上体验到。
合着这个新特性在 TS 里就可以愉快的将形如
${XX}
当成一种新的类型来做类型抽象了, 简单玩玩:00type HexNumber0x123 = `0x${123}`; 01 02type DOMEvent<Name extends string> = ( 03 `on${Name}` 04); 05 06// => 'onClick' 07type ClickEvt = DOMEvent<'Click'>; 08 09// 0 ~ F 10type FROM_0_TO_F = ( 11 '0' | '1' | '2' | '3' | 12 '4' | '5' | '6' | '7' | 13 '8' | '9' | 'A' | 'B' | 14 'C' | 'D' | 'E' | 'F' 15); 16 17type Hex = `${FROM_0_TO_F}${FROM_0_TO_F}`; 18 19// 形如 0xAABBCC 20type CharHex = `0x${Hex}`; 21 22const Char_A: CharHex = `0x41`; 23const Char_A_Err: CharHex = `0xGG`; // type error
Template Type 配合条件类型就可以来做一些匹配字符串的判断,如果再加上条件类型的递归即可实现一些文法处理器了,比方说有大神已经写出了加法计算器了,还有些人写出了 URL Parser,总之… 本文主要探讨这些 Parser 的写作手法。
在本文进一步之前,我们先从几个常用 type 来进一步熟悉 template type 这个特性
# Numberic :: Template + Union Type ↵
temaplte 结合 union 可以用来做判断数字的逻辑
00// 长度为 1 的字符串数字 01type Numberic = ( 02 '0' | '1' | '2' | '3' | '4' | 03 '5' | '6' | '7' | '8' | '9' 04);
上述 Numberic 可以用来限制一位长度的字符串,但不能限制 n 位长度的字符串类型,如果要实现 n 位的判读,需要引入递归操作:
00// 判断某字符串是不是数字 01type IsAllNumberic<S extends string> = ( 02 S extends `${Numberic}${infer Rest}` 03 // S 满足上面的 template type 的时候 04 // 如果 Rest 是 '' 则返回 true 否则递归返回 IsAllNumberic<Rest> 05 ? Rest extends `` ? true : IsAllNumberic<Rest> 06 // 不满足返回 false 07 : false 08) 09 10type testOneNumberic_1 = IsAllNumberic<'aaaa'>; // false 11type testOneNumberic_2 = IsAllNumberic<'12345'>; // true
# HeadOf 类型 :: Template infer ↵
00// 长度为 1 的字符串数字 01type Numberic = ( 02 '0' | '1' | '2' | '3' | '4' | 03 '5' | '6' | '7' | '8' | '9' 04); 05 06// 取头部 07// HeadOf<'1234'> ==> '123' 08type HeadOf<S extends string> = S extends `${infer Before}${Numberic}` ? Before : never;
取出头部 HeadOf 是很常用的工具,比方说实现加法里面 HeadOf 就是数字的除十操作。
# Tail 类型 :: Template 条件 infer 递归 ↵
00// 取尾部 01// Tail<'1234'> ===> '4' 02type Tail<S extends string> = ( 03 S extends `${Numberic}${infer Rest}` 04 // 满足上面的情况下且 Rest 是 `` 的时候 说明已经到底了 返回 S 即可,否则递归处理 05 ? Rest extends `` ? S : Tail<Rest> 06 : never 07);
取出尾部 Tail 也是很常用的工具,比方说实现加法里面 Tail 就是数字的对十取模操作 (求个位数)。
# Trim 类型 :: Template 条件 infer 递归 ↵
按照 js 的语意实现对字符串类型的 trim :
00// 工具函数 01// Trim<" var Eczn "> ===> "var Eczn" 02// 先递归 TrimRight 再 TrimLeft 03type Trim<S extends string> = ( 04 S extends `${infer Before} ` 05 ? Trim<Before> 06 : ( 07 S extends ` ${infer After}` 08 ? Trim<After> 09 : S 10 ) 11); 12 13type testTrim = Trim<' var Eczn '>; 14// => 'var Eczn'
引入 trim 是为了处理字符串两端中多余的空格
# KVPair 的定义和拍平 ↵
在处理复杂类型问题的时候总要引入点中间类型来方便转换,比方这里介绍的 KVPair, 他长这样:
00type KVP<K, V, Rest> = [K, V, Rest]; 01 02// 递归定义了一下 03type test = KVP<'key1', 1, KVP<'key2', 2, null>>; 04// 上面这个通过某种递归转换可以拍平为 { key1:1, key2:2 } 05// 这个变换后文会提到
这个结构其实跟 Scheme 里的 cons 结构是类似的,可以用来做函数式数据结构,额 … 总之这个结构可以不需要下标即可遍历,在 TS 里做遍历很重要的一种结构
上面应该已经提过了中间结构 KVP,我们需要利用这种结构生成对象类型出来,比如:
00// 下面这个可以拍平为 { k1: 123 } 01type p1 = ["k1", "123", null]; 02 03// 下面这个可以拍平为 { k1: 123, k2: "2" } 04type p2 = [ 05 "k1", "123", 06 ["k2", '"2"', null] 07]; 08 09// 下面这个可以拍平为 { k1: 123, inner: { k3: 3 }, k2: "2" } 10type p3 = [ 11 "k1", "123", 12 ["inner", ["k3", "3", null], ["k2", '"2"', null]] 13];
那么如何从 KVPair 里生成对象呢 ? 我们看到 KVPair 是递归的,可以通过递归遍历的形式来遍历操作,在遍历的过程中即可实现转对象的操作
00// 判断 V 是不是 KVP 结构,是的话用 KVPairsToObj 处理,否则返回其自身 01type MaybeKVP<V> = V extends [string, string, any] ? KVPairsToObj<V> : V; 02 03// 将递归的 KVParis 压平 ... 04type KVPairsToObj<P> = P extends [infer K, infer V, infer Rest] ? ( 05 // 先确保 K 可以作为 Key ... 否则我们返回 P 自己 06 K extends (string | number | symbol) ? ( 07 Rest extends null 08 // 如果说 Rest 是 null ... 则说明递归到底了, 直接返回 { [K]: V } 即可, 但 V 也有可能是 KVP 因此要 MaybeKVP<V> 递归一下 09 ? { [INNERKEY in K]: MaybeKVP<V> } 10 // 如果说 Rest 有东西则说明我们需要递归进一步处理 11 // 注意 INNERKEY 的取值为当前的 K 以及 keyof KVPairsToObj<Rest>, 是的需要在递归里面用到递归的结果 12 // 然后判断 INNERKEY 是不是 K 如果是的话返回 MaybeKVP<V> 13 // 否则返回 KVPairsToObj<Rest>[INNERKEY] 14 : { [INNERKEY in K | keyof KVPairsToObj<Rest>]: INNERKEY extends K ? MaybeKVP<V> : KVPairsToObj<Rest>[INNERKEY] } 15 ) : never 16) : never;
注意上面的
INNERKEY
那块,仔细揣摩 KVPairsToObj<Rest>
的含义以及 KVPairsToObj<Rest>[INNERKEY]
代表什么就能看懂了。另外推荐在 Eczn’s TypeScript Playground :: KVPairs 里查看上述代码,KM 这里代码显示不好看。# JSON Parser 的实现 ↵
实现 JSON Parser 关键是从从 raw json string 生成 KVPair,然后借助前文提到的拍平 KVPair 的工具类型去拍平为对象类型:
00// 从 " { XXXX } " 中取出 XXXX 来 (即取出 json kv string map) 01type SplitKVStringFrom<S extends string> = S extends `${infer AnyBlank}{${infer INNER}}${infer AnyBlank}` ? KVPairs<INNER> : Trim<S>; 02 03// 从 json kv string map 生成一种递归的结构 Pair = [K, V, Pair<string, any>] 表达一个对象的信息 04type KVPairs<S extends string> = ( 05 S extends `${infer AnyBlank}"${infer Key}"${infer AnyBlank}:${infer Rest}` 06 ? Rest extends `${infer Value},${infer Rest}` ? [Key, SplitKVStringFrom<Value>, KVPairs<Rest>] : [Key, SplitKVStringFrom<Rest>, null] 07 : Trim<S> 08) 09 10// 原始信息 11type myjson = '{ "k1" :"v1" , "wow": { "eczn": "喵" }, "k2": "v2","k3":333, "rrr": { "inner": "abc" } }'; 12 13// 中间 AST 表达 14type myparis = KVPairs<myjson>;
最后一步将将 KVPair 转对象类型,这里需借助 前文的 KVPairsToObj 即可实现,详见在线 DEMO: Eczn’s TypeScript Playground :: Type Parser For JSON
本文目的不是实现无懈可击的 json parser 而是介绍 template 写 parser 的思路,上述实现并不完整还差数字和数组没有处理,只实现了一个子集,有兴趣的同学可以自行补全这块。

enter image description here

enter image description here
# 实现加法算数运算 ↵
算数运算主要还是半加器、进位等这套来做的,当然也可以利用 Church Encoding 来实现,我再实现的过程中用了前者的思路:
首先要描述个位数,半加器要用
00// 描述个位数 01type Numberic = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
然后需要定义个位数的加法,也就是定义好半加器,而定义需要下点小手段,穷举定义的话会比较多代码,简化一下利用递归自增的形式来定义个位数的加法:
00// 定义 0 ~ 18 自增规则 01// 如 IncOne<'9', '1'> ===> '10' 02type IncOne<A extends string> = ( 03 A extends '0' ? '1' : // 0 + 1 = 1 04 A extends '1' ? '2' : // 1 + 1 = 2 05 A extends '2' ? '3' : // 2 + 1 = 3 06 A extends '3' ? '4' : // 3 + 1 = 4 07 A extends '4' ? '5' : // 4 + 1 = 5 08 A extends '5' ? '6' : // 5 + 1 = 6 09 A extends '6' ? '7' : // 6 + 1 = 7 10 A extends '7' ? '8' : // 7 + 1 = 8 11 A extends '8' ? '9' : // 8 + 1 = 9 12 A extends '9' ? '10' : // 9 + 1 = 10 13 A extends '10' ? '11' : // 9 + 1 = 10 14 A extends '11' ? '12' : // 9 + 1 = 10 15 A extends '12' ? '13' : // 9 + 1 = 10 16 A extends '13' ? '14' : // 9 + 1 = 10 17 A extends '14' ? '15' : // 9 + 1 = 10 18 A extends '15' ? '16' : // 9 + 1 = 10 19 A extends '16' ? '17' : // 9 + 1 = 10 20 A extends '17' ? '18' : // 9 + 1 = 10 21 A extends '18' ? '19' : // 9 + 1 = 10 22 never 23); 24 25// 由 IncOne 定义 0 ~ 9 范围内的任意加法 [0, 9] + [0, 9] 26// 如 IncX<'9', '9'> ===> '18' 27type IncX<A extends string, B extends string> = ( 28 B extends '0' ? A : // 0 + 1 = 1 29 B extends '1' ? IncX<IncOne<A>, '0'> : // 1 + 1 = 2 30 B extends '2' ? IncX<IncOne<A>, '1'> : // 2 + 1 = 3 31 B extends '3' ? IncX<IncOne<A>, '2'> : // 3 + 1 = 4 32 B extends '4' ? IncX<IncOne<A>, '3'> : // 4 + 1 = 5 33 B extends '5' ? IncX<IncOne<A>, '4'> : // 5 + 1 = 6 34 B extends '6' ? IncX<IncOne<A>, '5'> : // 6 + 1 = 7 35 B extends '7' ? IncX<IncOne<A>, '6'> : // 7 + 1 = 8 36 B extends '8' ? IncX<IncOne<A>, '7'> : // 8 + 1 = 9 37 B extends '9' ? IncX<IncOne<A>, '8'> : // 9 + 1 = 10 38 A 39);
然后取出个位数和十位数的这些可以使用本文一开始的 HeadOf 和 Tail,最后整理一下:
00// 加法: A B 为加数, C 是进位, 默认为 0 01type Add<A extends string, B extends string, C extends string = '0'> = ( 02 A extends '' ? C extends '1' ? Add<B, C> : B : 03 B extends '' ? C extends '1' ? Add<A, C> : A : 04 A extends '0' ? C extends '1' ? Add<B, C> : B : 05 B extends '0' ? C extends '1' ? Add<A, C> : A : 06 // @ts-ignore 这块递归比较多 TS 会有警告, 故加了个 ts-ignore 07 `${Add< HeadOf<A>, HeadOf<B>, HeadOf<IncX<Tail<A>, Tail<B>>> >}${Tail< IncX<IncX<Tail<A>, Tail<B>>, C> >}` 08);

enter image description here
乘法的计算可以利用加法递归定义;求余其实就是 Tail 取模其实是 HeadOf … 总之其他的运算都还算可以做。
# 实现 URL Parser ↵
根据 URL 的定义可以很快写出:
00type MakeUrl<Protocol, Domain, Port, Pathname, Search, Hash> = { 01 protocol: Protocol, 02 domain: Domain, 03 port: Port, 04 pathname: Pathname, 05 search: Search, 06 hash: Hash 07}; 08 09type URLParse<U extends string> = ( 10 U extends `${infer Protocol}://${infer Domain}:${infer Port}/${infer Pathname}` ? 'AA' : 'BB' 11 // 这里停顿一下... Port 是可省略的 ... 这里又必填了 12)
写到这里发现 URL 里有些字段是可以不用填的,因此上面的做法很局限,不好搞全全部情况。
斟酌之后考虑用 KVPair 的结构来解析:
00// 解析 URL 为对象类型 01type URLParse<U> = KVPairsToObj<URLKVPairs<U>>; 02 03// 解析 URL 为 KVPairs 04type URLKVPairs<U> = ( 05 U extends `${infer Protocol}://${infer Rest}` ? ['protocol', Protocol, ParseDomain<Rest>] : 06 U extends `//${infer Rest}` ? ['protocol', null, ParseDomain<Rest>] : 07 never 08); 09 10// 解析 Domain 11type ParseDomain<U> = ( 12 U extends `${infer Domain}:${infer Port}/${infer Rest}` ? ['domain', Domain, ['port', Port, ParsePathname<Rest>]] : 13 U extends `${infer Domain}:${infer Port}` ? ['domain', Domain, ['port', Port, null]] : 14 U extends `${infer Domain}/${infer Rest}` ? ['domain', Domain, ['port', null, ParsePathname<Rest>]] : 15 ['domain', U, null] 16) 17 18// 解析 Pathname 19type ParsePathname<U> = ( 20 U extends `${infer Pathname}?${infer Search}#${infer Hash}` ? ['pathname', Pathname, ['search', Search, ['hash', Hash, null]]] : 21 U extends `${infer Pathname}?${infer Search}` ? ['pathname', Pathname, ['search', Search, null]] : 22 ['pathname', U, null] 23)
总之我的思路就是想办法得到 KVPair 最后 pair 转 obj
00https://eczn.local/a.html?aaa=bbb#hash 01=> ['protocol', 'https', ParseDoamin<'eczn.local/a.html?aaa=bbb#hash'>] 02 03ParseDoamin<'eczn.local/a.html?aaa=bbb#hash'> 04=> ['domain', 'eczn.local', ['port', null, ParsePathname<'a.html?aaa=bbb#hash'>]] 05 06ParsePathname<'a.html?aaa=bbb#hash'> => 07['pathanme', 'a.html', ['search', 'aaa=bbb', ['hash', 'hash', null]]] 08 09最后是一个 [K, V, [K2, V2, [K3, V3, null]]] 的递归结构,将其转为 obj 即可(前面 JSON Parser 已经实现了)
# 实现 QueryString Parser ↵
上面 URL Parser 并没有帮忙 parse querystring,这里也补一个, 道理也是类似的,从字符串生成 KVP 再转 object
00// Query 字符串转 KVP 结构 01type QueryParseToKVP<S extends string> = ( 02 S extends `${infer Key}=${infer Value}&${infer Rest}` 03 ? [Key, Value, QueryParseToKVP<Rest>] // 这种情况直接解析 04 : S extends `${infer Key2}=${infer Value2}` 05 ? [Key2, Value2, null] // 这种情况代表到最后一项了 06 : null // 否则返回 null 07);
# 实现 UUID Type Check ↵
校验某字符串是否满足 uuid 的规则:
00type StrHex = '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '0' | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'a' | 'b' | 'c' | 'd' | 'e' | 'f'; 01 02// 返回长度, 如 StrHexCons<'1234'> ===> '4' 03type StrHexCons<S extends string, ACC extends keyof AddOneMap = '0'> = ( 04 S extends `${StrHex}${infer Rest}` 05 ? StrHexCons<Rest, AddOneMap[ACC]> 06 : ACC 07); 08 09type AddOneMap = { 10 '0': '1', 11 '1': '2', 12 '2': '3', 13 '3': '4', 14 '4': '5', 15 '5': '6', 16 '6': '7', 17 '7': '8', 18 '8': '9', 19 '9': '10', 20 '10': '11' 21 '11': '12' 22 '12': '13', 23 '13': never, // 12 对应 13 ... 会导致 AddOneMap[keyof AddOneMap] 出现问题 24} 25 26type IsUUID<S extends string> = ( 27 S extends `${infer One}-${infer Two}-${infer Three}-${infer Four}` 28 ? ( 29 '8' extends StrHexCons<One> ? 30 '4' extends StrHexCons<Two> ? 31 '4' extends StrHexCons<Three> ? 32 '12' extends StrHexCons<Four> ? 33 true 34 : false 35 : false 36 : false 37 : false 38 ) 39 : false 40); 41 42type __a = IsUUID<'12345678-1234-1234-15123456789ABC'>; // false 43type __b = IsUUID<'12345678-1234-1234-123456789ABC'>; // true 44type __c = IsUUID<'123456**-1234-1234-123456789ABC'>; // false 45type __d = IsUUID<'12345678-1&24-1234-123456789ABC'>; // false
# Template Type 小结 ↵
私以为的 Template Type 的几个关键点 :
- 静态的 Template Pattern 产生了变化的 string 字面量
- 利用 KVP 结构来做 Parser / 遍历事半功倍
- 类型系统完全可以当成单独的语言系统来看待, 可以被形式化验证