TypeScript类型系统中如何处理类型参数新鲜度,兼容非新鲜变量
解决TypeScript中新鲜字符串与非新鲜字符串的长度类型校验问题
基于字符串长度计算的TypeScript类型实现,我编写了以下校验字符串长度的代码:
type LengthOfString<T extends string, L extends any[] = []> = T extends `${string}${infer R}` ? LengthOfString<R, [...L, string]> : L["length"] type StringOfLength<T extends string, N extends number> = LengthOfString<T> extends N ? T : never function isStringOfLength<T extends string, N extends number>(str: T, len: N): str is StringOfLength<T, N> { return str.length === len } function takeString<T extends string>(str: StringOfLength<T, 5>) { if (!isStringOfLength<T, 5>(str, 5)) throw Error("String length !== 5") console.log(str) } // 正常运行(新鲜字符串字面量) takeString("22222") // 编译报错(新鲜字符串字面量长度不符) takeString("2222") let str = "22222" // 编译报错——非新鲜变量无法通过类型校验 takeString(str)
问题核心:新鲜字符串字面量能被TypeScript推断出具体长度,可通过编译时类型校验;但非新鲜变量会被推断为string类型,LengthOfString<string>结果为number,无法匹配5,导致StringOfLength<string, 5>成为never,触发编译报错。
解决方案:用函数重载兼顾编译时与运行时校验
通过定义双重载函数,让新鲜字符串走编译时校验逻辑,普通string类型走运行时校验逻辑:
type LengthOfString<T extends string, L extends any[] = []> = T extends `${string}${infer R}` ? LengthOfString<R, [...L, string]> : L["length"] type StringOfLength<T extends string, N extends number> = LengthOfString<T> extends N ? T : never // 重载1:针对新鲜字符串字面量,编译时校验长度 function takeString<T extends string>(str: StringOfLength<T, 5>): void; // 重载2:针对普通string类型,仅做运行时校验 function takeString(str: string): void; // 函数实现 function takeString(str: string) { if (str.length !== 5) throw Error("String length !== 5") console.log(str) } // 正常运行(新鲜字符串) takeString("22222") // 编译报错(新鲜字符串长度不符) takeString("2222") let str = "22222" // 编译通过,运行时校验 takeString(str) let wrongStr = "123" // 编译通过,运行时报错 takeString(wrongStr)
方案说明
- 传入新鲜字符串字面量时,TypeScript匹配第一个重载,通过
StringOfLength类型在编译阶段检查长度,不符合直接报错。 - 传入非新鲜变量(类型为
string)时,TypeScript匹配第二个重载,编译阶段不做长度校验,转而在运行时通过str.length判断合法性。
如果要保留原有的isStringOfLength类型守卫,可调整实现如下:
type LengthOfString<T extends string, L extends any[] = []> = T extends `${string}${infer R}` ? LengthOfString<R, [...L, string]> : L["length"] type StringOfLength<T extends string, N extends number> = LengthOfString<T> extends N ? T : never function isStringOfLength<T extends string, N extends number>(str: T, len: N): str is StringOfLength<T, N> { return str.length === len } // 重载定义 function takeString<T extends string>(str: StringOfLength<T, 5>): void; function takeString(str: string): void; function takeString(str: string) { if (!isStringOfLength(str, 5)) throw Error("String length !== 5") console.log(str) }
这样既保留了类型守卫逻辑,也实现了两种场景的兼容。
内容的提问来源于stack exchange,提问作者Tristan F.-R.
相关产品推荐
相关产品推荐

