简介
本文第一部分主要是实现大数版减法和斐波那契数列的计算, 第二部分介绍类型体操在实际项目中的应用, 以wasm中的cwrap/ccall函数为例子, 想保留发量的同学可以看应用部分, 水平有限, 要是有理解和表达不恰当的欢迎评论交流哈~ 实现效果如下
当时之所以想实现fib是因为一般图灵完备的语言都可以实现, 并且一般能实现fib的也就可以看做是图灵完备的, 因为至少拥有了顺序, 循环和分支. 像HTML/CSS就不行(用css变量有没有搞头?) 【🤦♂️工作无用】证明 JS 和 TS 类型编程是图灵完备的 - 掘金
原始版本
绕了一些弯路, 但是基本思想还是用不同长度的元组代表数字, 数的表示可以用任意形式, 比如元组的嵌套深度或者字符串的长度, 这里主要是为了可读性选择元组 用ts类型系统实现斐波那契数列 - 掘金
简单版本1
主要思想是将减法 a-b = c, 转换为 a = b + c 的形式
// @ts-nocheck
type Len<T extends any[]> = T["length"];
// @ts-ignore
type IncList<T extends any[]> = [any, ...T];
// @ts-ignore
type AddList<A extends any[], B extends any[]> = [...A, ...B];
type FromLen<T extends number, A extends any[] = []> = Len<A> extends T
? A
: FromLen<T, IncList<A>>;
export type Add<A extends number, B extends number> = Len<
AddList<FromLen<A>, FromLen<B>>
>;
type SubList<A extends any[], B extends any[], C extends any[] = []> = Len<
A
> extends Len<AddList<B, C>>
? C
: SubList<A, B, IncList<C>>;
export type Sub<A extends number, B extends number> = Len<
SubList<FromLen<A>, FromLen<B>>
>;
export type Fib<N extends number> = N extends 0
? 0
: N extends 1
? 1
: // @ts-ignore
Add<Fib<Sub<N, 1>>, Fib<Sub<N, 2>>>;
简单版本2
去除了一些没必要的代码, 使用infer实现了减一和减二, 因为我们默认是大减去小 , 不会出现不够减的情况
// 辅助函数,暂时不用关心
type NumberToArray<T, I extends any[] = []> = T extends T ? I['length'] extends T ? I : NumberToArray<T, [any, ...I]> : never;
type Add<A, B> = [...NumberToArray<A>, ...NumberToArray<B>]['length']
type Sub1<T extends number> = NumberToArray<T> extends [infer _, ...infer R] ? R['length'] : never;
type Sub2<T extends number> = NumberToArray<T> extends [infer _, infer __, ...infer R] ? R['length'] : never;
// 计算斐波那契数列
type Fibonacci<T extends number> =
T extends 1 ? 1 :
T extends 2 ? 1 :
Add<Fibonacci<Sub1<T>>, Fibonacci<Sub2<T>>>;
type Fibonacci9 = Fibonacci<9>;
/** 得到结果
type Fibonacci9 = 34
*/
限制
上述的实现本质上还是依赖于动态生成的元组, 但是由于递归深度问题, 即使是4.5优化了尾递归, 依然无法计算比较大的项 , ts4.4不到100就g, 4.5达不到1000, 所以展示案例一般都是fib(10) 因为这是4.4支持的最大数字了
type NumberToArray<T, I extends any[] = []> = T extends T ? I['length'] extends T ? I : NumberToArray<T, [any, ...I]> : never;
type r1 = NumberToArray<100>
type r2 = NumberToArray<1000>
大数版本
之前看到有同学实现了大数加法, 就想照此思路进行一些改进
大数加法
基本思想用打表实现单位数字加法, 然后用元组表示多位数, 用类型系统实现了一个加法器 用 TS 类型系统实现大数加法 - 知乎
大数减法
有了加法基本上就可以写操作系统了🐶 这里用补码实现减法, 在加法的基础上加了一点点工具函数(这里使用数字的高位在数组高位, 但是字符串中为了可读性使用的是数字高位在字符串低位) 默认大减去小, 不需要考虑负数情况, 所以小数字的补码需要和大数字位数一致, 然后要考虑高位有多余的0时需要去除
type ComplementMap = [9, 8, 7, 6, 5, 4, 3, 2, 1];
type DigitListToComplement<
T extends any[],
MAX extends any[],
R extends any[] = []
> = T extends [infer First, ...infer Rest]
? DigitListToComplement<
Rest,
[...R, First extends number ? ComplementMap[First] : "N"]
>
: R;
type ToSameLen<
T extends any[],
MAX extends any[],
R extends any[] = []
> = MAX["length"] extends 0 ? R : ToSameLen<Shift<T>, Shift<MAX>, [...R, T[0]]>;
type isNever<T extends any> = T extends never
? never extends T
? true
: false
: false;
type isUndefined<T extends any> = T extends undefined
? undefined extends T
? true
: false
: false;
type isUnknown<T extends any> = T extends unknown
? unknown extends T
? true
: false
: false;
type ToComplement<T extends any> = {
[k in keyof T]: isNever<T[k]> extends true
? 9
: isUndefined<T[k]> extends true
? 9
: ComplementMap[T[k]];
};
type TrimRightZero<T extends any[]> = T extends [0]
? [0]
: T extends [infer N]
? [N]
: T extends [infer P, ...infer W, 0]
? TrimRightZero<[P, ...W]>
: T;
type SubDigitList<A extends any[], B extends any[]> = TrimRightZero<
Pop<AddDigitList<AddDigitList<A, ToComplement<ToSameLen<B, A>>>, [1]>>
>;
Fib
有了加减法其实Fib很简单了, 就是加减的组合, 最后再对string和元组包装一层就可以了
type FibDigitList<T extends any[] = []> = T extends [0]
? [0]
: T extends [1]
? [1]
: AddDigitList<
FibDigitList<SubDigitList<T, [1]>>,
FibDigitList<SubDigitList<T, [2]>>
>;
type Fib<s extends string> = DigitListToString<FibDigitList<ToDigitList<s>>>;
declare function fib<T extends string>(n: T): Fib<T>;
const f50 = fib("50");
完整代码
TypeScript: TS Playground - An online editor for exploring TypeScript and JavaScript
为wasm中的ccall和cwrap实现类型推断
完整代码参考 emscripten , 可以安装 @types/emscripten - npm 为项目中的wasm加上类型支持, 使用中有问题也欢迎提PR一起改进wasm的开发体验
原始定义
可以看到原始版本直接用的any, 这样就丢失了类型信息, 可能会导致参数顺序传错的问题o(╥﹏╥)o
declare function ccall(
ident: string,
returnType: Emscripten.JSType | null,
argTypes: Emscripten.JSType[],
args: Emscripten.TypeCompatibleWithC[],
opts?: Emscripten.CCallOpts,
): any;
declare function cwrap(
ident: string,
returnType: Emscripten.JSType | null,
argTypes: Emscripten.JSType[],
opts?: Emscripten.CCallOpts,
): (...args: any[]) => any;
类型映射 主要目的是将字符串字面量'number' 映射为类型 number, 这里直接用一个map就行了
export type DataMap<R extends any> = R extends Emscripten.JSType
? {
number: number;
string: string;
array: number;
boolean: boolean;
}[R]
: never;
返回值推断
返回值是单个类型, 推断其实相当于从Map取对应的类型
type CvtReturn<R extends Emscripten.JSType | null> = R extends null
? null
: DataMap<Exclude<R, null>>;
参数推断
这里坑了好久, 参数接收的是一个字符串字面量的元组, 返回的是对应的类型元组, 一切都很美好
export type CvtArg<T extends readonly Emscripten.JSType[]> = Extract<
{
[P in keyof T]: DataMap<T[P]>;
},
any[]
>;
然而当你实现后发现有问题. 用个简单的例子说明下, 可以发现类型 退化 了, 从字面量元组变成了字符串数组, 所以导致后续的推导失败, 这里不太清楚是什么机制导致的, 有同学知道的话欢迎交流哈
export declare function cwrap<
I extends readonly Emscripten.JSType[]
R extends Emscripten.JSType | null
>(
ident: string,
returnType: R,
argTypes: I,
opts?: Emscripten.CCallOpts
): (...arg: CvtArg<I>) => CvtReturn<R>;
也想了一些办法, 但都没能解决, 后来在大佬的提醒下去看了NAPI的实现, 里面类似的函数, 经过一番摸索, 发现他们有一个很奇怪的操作, 虽然看不懂, 但确实能解决问题, 要是有能看懂的同学, 也欢迎交流哈 他们加了一个空数组(元组?), 然后就居然可以推导出来了 ???!!!
ccall
其实是cwrap的变种, 多了一个实参列表, 这里实参也是有类型约束的, 妈妈再也不用担心我传错参数了(o(╥﹏╥)o (就是因为有次传错了参数导致了一个极其诡异的问题, 所以才有了对wasm加类型的想法)
export declare function ccall<
I extends readonly Emscripten.JSType[] | [],
R extends Emscripten.JSType | null
>(
ident: string,
returnType: R,
argTypes: I,
args: CvtArg<I>,
opts?: Emscripten.CCallOpts
): CvtReturn<R>;
个人感想
遇到卡住很久的问题最好请教大佬或者参考业界已有实现, 不然自己折腾有时候很费时间 其实对于类型体操可能大部分开发者都不会实际使用到, 但是类型体操能够让我们明确TS拥有怎样的能力, 以及能够解决怎样的问题, 当我们真正遇到复杂的推断时能够想到可以使用TS解决而不是直接用any, 体操的最终目的还是提升开发体验和减少bug