As we all know, TypeScript's type system is often jokingly called "type gymnastics" because of its high flexibility. Various experts have rolled up on the type system and realized various incredible functions.
Recently, Uncle Xu Fei also wrote a Chinese chess, which can be said to be very voluminous. zhuanlan.zhihu.com/p/426966480
In fact, complex type operations are not traceless. This paper attempts to tap the potential of type system from the perspective of metaprogramming, hoping to help you catch some ideas and context.
The basis of metaprogramming is Turing complete subsystem. Is TypeScript type system Turing complete? Of course, the answer is yes.
Extensions of TypeScript type system? It forms the ability of branching, while allowing recursion forms the ability of loop. In addition, the type dependency itself can form a sequential structure, which meets the requirements of Turing's completeness.
The basic types of TypeScript include Number, Boolean, String, Tuple, etc. complex types include functions and objects. Although Turing is complete in theory, we still need some basic operation support.
Tuple operation
The core of tuple operation is... Operation and infer type,... Tuples can be expanded to construct new tuples, and infer allows us to segment matching from tuples and obtain each part of them.
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]; Copy code
Of course, in fact, these methods have no meaning. In actual use, we don't need such abstraction. Just write the expression on the right. Here we are just familiar with the characteristics of tuples as a simple warm-up exercise.
... and infer are almost equal to addition and subtraction in tuple operation, and are the basis of all subsequent complex operations.
recursion
Recursion is the cornerstone of all complex operations. In the absence of subtraction and comparison operations, we can only use the length of tuples and extensions to realize comparison. The following code forms a fixed length list:
type List<Type, n extends number, result extends any[] = []> = result['length'] extends n ? result : List<Type, n, [...result, Type]>; Copy code
A more complex example can be seen from the name:
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 ; Copy code
String related operations
String types are similar to tuples. They are "first-class citizens" in the type system. Through template matching, we can intercept various parts of the string. The following are some examples. The function names are familiar to everyone, so we won't explain them here.
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>}` : '' Copy code
Code style
Because there is no statement, you can only use extends?: Structure, it becomes very difficult to write clearly structured code. Here I recommend an indentation style I like to use.
Rule 1: serial structure
Try to make nested extensions?: It appears in the false branch. In this way, we can use a question mark to correspond to a result. All extensions are not indented. This structure is similar to switch case, such as:
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 Copy code
Rule 2: consolidation conditions
When extensions?: If it appears in the true branch, the rows can be merged if necessary, and the value of the row should always be maintained? And: are equal and end with a colon, such as:
type and<v1 extends boolean, v2 extends boolean> = v1 extends true ? v2 extends true ? true : false : false Copy code
Rule 3: nested structure
When extensions?: In the true branch, you can break the line after the question mark and indent the next line
type or<v1 extends boolean, v2 extends boolean> = v1 extends false ? v2 extends true ? true : false : true Copy code
number operation
Although TypeScript supports constants, it is not friendly in itself. It can hardly perform any operation in the type system. There is no addition and subtraction method in itself, but we can convert number into an array to do some simple operations:
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]>; Copy code
The ['length '] of tuple is used to simulate various operations of number, which can meet some operation requirements when the number is small. If you want to carry out large number operation, you need to convert it into a string through the previous numberToString. You can refer to this article zhuanlan.zhihu.com/p/423175613
Use binary to represent integers
A more scientific and reasonable approach is to use binary to represent integers, which can be used to do some large-scale calculations
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]> Copy code
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]>>; Copy code
Small trial ox knife
Well, the above examples are relatively simple and seem to lack some flavor of meta programming. Let's challenge: write an AI of TicTacToe.
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 Copy code
www.typescriptlang.org/play?ts=4.5...

Well, if you see here, I believe you have a preliminary understanding of TypeScript type metaprogramming. Next, you can flexibly apply it to your daily work.