你所不知道的TypeScript的类型系统的用途
TypeScript编译器被设计用来做初学者的任务,比如说类型检查与一些类型操作。但如果你仔细观察--我们会的--我们可以在编译时做真正的工作。
是的,你没看错,是在编译时!我们选择的武器是TypeScript。我们选择的武器是TypeScript类型系统。我们可以使用TypeScript类型系统在编译时进行大量计算。我们能走多远呢?让我们疯狂一下吧!
让我们尝试在编译时计算一些真正大的斐波那契数。
斐波那契数
斐波那契数是以递归方式定义的。
Fib(0) = 0Fib(1) = 1Fib(n) = Fib(n-1) + Fib(n-2)
这是一个完美的问题,可以看到 TypeScript 的类型系统有多么强大。首先,我们需要一些基本操作。在开始时,我们从对个位数的运算开始。
个位数的基本算术运算
最简单的算术操作是增加或减少一个数字。因此,让我们定义一个递减类型Decrement<Digit> ,它可以从一个个位数中减去1。
type Decrement<Digit> = Digit extends 1 ? 0
: Digit extends 2 ? 1
: Digit extends 3 ? 2
: Digit extends 4 ? 3
: Digit extends 5 ? 4
: Digit extends 6 ? 5
: Digit extends 7 ? 6
: Digit extends 8 ? 7
: Digit extends 9 ? 8
: 'unsupported digit';
我们如何检查我们的递减类型是否有效?我们如何输出产生的类型字?一种可能性是引发一个类型错误。引起类型错误的最简单的方法是将错误的类型分配给结果类型。
const result: Decrement<4> = {} // Type '{}' is not assignable to type '3'.
同样,我们可以创建一个Increment<Digit> 类型来增加一个个位数。
type Increment<Digit> = Digit extends 0 ? 1
: Digit extends 1 ? 2
: Digit extends 2 ? 3
: Digit extends 3 ? 4
: Digit extends 4 ? 5
: Digit extends 5 ? 6
: Digit extends 6 ? 7
: Digit extends 7 ? 8
: Digit extends 8 ? 9
: 'unsupported digit';
这很容易。现在我们可以使用增量和减量类型来增加一个Addition 类型。
可以将加法实现为一个递归类型,在第一个操作数上加一,在第二个操作数上减一。之后用结果调用加法。如果第二个操作数达到0,第一个操作数就是原来操作数的总和。
type Addition<Augend, Addend>
= Addend extends 0 ? Augend : Addition<Increment<Augend>, Decrement<Addend>>
这就是了!现在我们可以计算一些斐波那契数了。
在编译时计算斐波那契数
type Decrement<Digit> = Digit extends 1 ? 0
: Digit extends 2 ? 1
: Digit extends 3 ? 2
: Digit extends 4 ? 3
: Digit extends 5 ? 4
: Digit extends 6 ? 5
: Digit extends 7 ? 6
: Digit extends 8 ? 7
: Digit extends 9 ? 8
: 'unsupported digit';
type Increment<Digit> = Digit extends 0 ? 1
: Digit extends 1 ? 2
: Digit extends 2 ? 3
: Digit extends 3 ? 4
: Digit extends 4 ? 5
: Digit extends 5 ? 6
: Digit extends 6 ? 7
: Digit extends 7 ? 8
: Digit extends 8 ? 9
: Digit extends 9 ? 10
: 'unsupported digit';
type Addition<Augend, Addend> = Addend extends 0 ? Augend
: Addition<Increment<Addend>, Decrement<Augend>>;
type Fib<N> =
// Fib(0) = 0 or Fib(1) = 1
N extends 0 | 1 ? N
// Fib(n) = Fib(n-1) + Fib(n-2)
: Addition<Fib<Decrement<N>>, Fib<Decrement<Decrement<N>>>>;
const result: Fib<6> = {} // Type '{}' is not assignable to type '8'.
通过上面的脚本,我们可以计算到第6个斐波那契数。这很好,但它不可能是这个主题的最后一个字。我们的目标应该是更多。
创建一个组合的数字格式
对于更大的数字,我们可以为Increment 和Decrement 类型创建巨大的列表,其中每个数字的增量或减量都被映射出来。但这是一个很大的工作,而且相当无聊。
一个更聪明的方法是使用分层的列表来表示更大的数字。
interface Num<Digit, Tail> {
value: Digit | Tail;
}
列表表示一个数字,从最后一个数字开始到最高一个数字,它的终点是null 。例如,数字142被表示为。
Num<2, Num<4, Num<1, null>>>
这个递归列表的Decrement 算子变成递归,规则如下。
- 10的递减量是9
- 如果一个数字以0结尾,最后一个数字将是9,数字的下一项被递减
- 否则我们只递减最后一位数字
我们的新数字格式的Increment 操作符遵循非常类似的规则。
- 9的增量是10
- 如果一个数字以9结尾,最后一位数字将是0,数字的下一项将被递增。
- 否则,我们只递增最后一位数字
更新后的斐波那契数程序--使用我们组成的数字格式--增加了上述的增量和减量功能。
interface Num<D, Tail> {
value: D | Tail;
}
type DecDigit<D> = D extends 1 ? 0
: D extends 2 ? 1
: D extends 3 ? 2
: D extends 4 ? 3
: D extends 5 ? 4
: D extends 6 ? 5
: D extends 7 ? 6
: D extends 8 ? 7
: D extends 9 ? 8
: 'unsupported digit';
type DecNum<N> =
// if N is 10, the result is 9
N extends Num<0, Num<1, null>> ? Num<9, null>
// if the number ends with 0, the last digit is 9 and the Tail is decremented
: N extends Num<0, infer Tail> ? Num<9, DecNum<Tail>>
// otherwise, just decrement the last digit
: N extends Num<infer D, infer Tail> ? Num<DecDigit<D>, Tail>
: 'N must be a valid number type';
type IncDigit<D> = D extends 0 ? 1
: D extends 1 ? 2
: D extends 2 ? 3
: D extends 3 ? 4
: D extends 4 ? 5
: D extends 5 ? 6
: D extends 6 ? 7
: D extends 7 ? 8
: D extends 8 ? 9
: 'unsupported digit';
type IncNum<N> =
// if N is 9, the result is 10
N extends Num<9, null> ? Num<0, Num<1, null>>
// if the last digit is 9 the last digit is 0 and the Tail is incremented
: N extends Num<9, infer Tail> ? Num<0, IncNum<Tail>>
// otherwise the last digit is incremented
: N extends Num<infer D, infer Tail> ? Num<IncDigit<D>, Tail>
: 'N must be a valid number type';
type Add<T, U> =
// if U is 0, T is the sum of the original operands
U extends Num<0, null> ? T
// else we increment the first and decrement the second operands
: Add<IncNum<T>, DecNum<U>>;
type Fib<N> =
// Fib(0) = 0 or Fib(1) = 1
N extends Num<0, null> | Num<1, null> ? N
// Fib(0) = 0 or Fib(1) = 1
: Add<Fib<DecNum<N>>, Fib<DecNum<DecNum<N>>>>;
const result: Fib<Num<8, Num<1, null>>> = {}
// Property 'value' is missing in type '{}' but required
// in type 'Num<4, Num<8, Num<5, Num<2, null>>>>'.
通过这种优化,我们能够计算到第18个斐波那契数,即2584!我们正在达到这个目标。我们正在走向成功。
但数字2584的表示方法是Num<4, Num<8, Num<5, Num<2, null>>>>,这很让人困惑。我们能不能让输入和输出的表述更易读?
对输出进行合理的格式化
为了使输出更易读,数字必须以自然的顺序排列,我们想摆脱所有这些Num<…> 类型。
type PrettyOutput<N> = N extends Num<infer D, infer Tail> ?
Tail extends null ? D extends number ? `${D}` : ''
: `${PrettyOutput<Tail>}${D extends number ? `${D}` : ''}`
: 'N must be a composed number type';
PrettyOutput 类型将一个数字从我们的列表表示法转化为具有数字自然排序的字符串。使用PrettyOutput 类型,我们的错误信息是
const result: PrettyOutput<Fib<Num<8, Num<1, null>>>> = {}
// Type '{}' is not assignable to type '2584'.
很好地格式化输入
同样,数字18的输入(Num<8, Num<1, null>>)也是不太容易读懂的。我们需要一个类似于Pretty 类型的概念,将数字的顺序颠倒过来,摆脱所有这些Num<…> 类型。
type PrettyInput<A, Tail = null> = A extends [infer D] ? Num<D, Tail>
: A extends [infer D, ...infer Rest] ? PrettyInput<Rest, Num<D, Tail>>
: 'N must be a tuple of digits';
如果我们将Pretty 和CreateNum 类型应用于我们的 Fibonacci 类型,数字就会变得更容易阅读。
const result: PrettyOutput<Fib<PrettyInput<[1, 8]>>> = {}
// Type '{}' is not assignable to type '2584'.
更巧妙的加法
如果我们试图计算斐波那契数19,我们会得到错误Type instantiation is excessively deep and possibly infinite 。TypeScript只允许我们进行一定深度的递归。由于是100,我们的加法只能到一定的深度。
许多递归在加法中被浪费了,因为要加上数字130+80,我们浪费了80次递归。如果我们仔细观察加法,就会发现加法不能把加数一直减到零。如果我们在最后一位数字中达到零就足够了。
此后,我们只关心倒数第二位。如果倒数第二位是零,我们只关心倒数第三位,以此类推。如果我们对加法进行优化,它需要做到以下几点。
- 如果第二个操作数是0,我们就完成了。将第一个操作数作为结果返回
- 如果第二个操作数以0结尾,我们只需要查看较高的数字
- 否则,增加第一个操作数的当前数字,并减去第二个操作数的当前数字。
interface Num<D, Tail> {
value: D | Tail;
}
type DecDigit<D> = D extends 1 ? 0
: D extends 2 ? 1
: D extends 3 ? 2
: D extends 4 ? 3
: D extends 5 ? 4
: D extends 6 ? 5
: D extends 7 ? 6
: D extends 8 ? 7
: D extends 9 ? 8
: 'unsupported digit';
type DecNum<N> =
// if N is 10, the result is 9
N extends Num<0, Num<1, null>> ? Num<9, null>
// so save some recursions decrement the digit if its the last
: N extends Num<infer D, null> ? Num<DecDigit<D>, null>
// if the number ends with 0, the last digit is 9 and the Tail is decremented
: N extends Num<0, infer Tail> ? Num<9, DecNum<Tail>>
// otherwise, just decrement the last digit
: N extends Num<infer D, infer Tail> ? Num<DecDigit<D>, Tail>
: 'N must be a valid number type';
type IncDigit<D> = D extends 0 ? 1
: D extends 1 ? 2
: D extends 2 ? 3
: D extends 3 ? 4
: D extends 4 ? 5
: D extends 5 ? 6
: D extends 6 ? 7
: D extends 7 ? 8
: D extends 8 ? 9
: 'unsupported digit';
type IncNum<N> =
// if N is 9, the result is 10
N extends Num<9, null> ? Num<0, Num<1, null>>
// so save some recursions increment the digit if its the last
: N extends Num<infer D, null> ? Num<IncDigit<D>, null>
// if the last digit is 9 the last digit is 0 and the Tail is incremented
: N extends Num<9, infer Tail> ? Num<0, IncNum<Tail>>
// otherwise the last digit is incremented
: N extends Num<infer D, infer Tail> ? Num<IncDigit<D>, Tail>
: 'N must be a valid number type';
type Add<T, U> =
// if U is 0, T is the sum of the original operands
U extends Num<0, null> ? T
: T extends Num<infer TD, infer TTail> ?
// if the second operand ends with 0, only look at the higher digits
U extends Num<0, infer UTail> ? Num<TD, Add<TTail, UTail>>
// otherwise increment the first and decrement the second operand
: Add<IncNum<T>, DecNum<U>>
: 'N must be a composed number type';
type Fib<N> =
// Fib(0) = 0 or Fib(1) = 1
N extends Num<0, null> | Num<1, null> ? N
// Fib(0) = 0 or Fib(1) = 1
: Add<Fib<DecNum<N>>, Fib<DecNum<DecNum<N>>>>;
type PrettyInput<A, Tail = null> = A extends [infer D] ? Num<D, Tail>
: A extends [infer D, ...infer Rest] ? PrettyInput<Rest, Num<D, Tail>>
: 'N must be a tuple of digits';
type PrettyOutput<N> = N extends Num<infer D, infer Tail> ?
Tail extends null ? D extends number ? `${D}` : ''
: `${PrettyOutput<Tail>}${D extends number ? `${D}` : ''}`
: 'N must be a composed number type';
const result: PrettyOutput<Fib<PrettyInput<[7, 0]>>> = {}
// Type '{}' is not assignable to type 190392490709135
哇,我们可以计算出第70个斐波那契数了!它是190'392'490'709'135 。这个数字甚至不适合32位的整数,而且还划伤了一个双变量的尾数。
故事时间
在我获得科学计算的硕士学位后,我曾在世界最大的超级计算机上部署高性能的C++应用程序。然后我成立了一家网络公司,并将目光转向了TypeScript。我从不认为 TypeScript 是一种简单的脚本语言,它可以与大家伙站在一起。