为什么TypeScript';编译器不分析类型数组吗?
设置:给定一些类型脚本代码,如:为什么TypeScript';编译器不分析类型数组吗?,typescript,Typescript,设置:给定一些类型脚本代码,如: type ObjectList = { [index: string]: string; }; function makeList(input: ObjectList | string | number): string[] { if (typeof input === "string" || typeof input === "number") { return [String(input)]; }
type ObjectList = {
[index: string]: string;
};
function makeList(input: ObjectList | string | number): string[] {
if (typeof input === "string" || typeof input === "number") {
return [String(input)];
}
const arr = [];
for (const x in input) {
arr.push(String(input[x]));
}
return arr;
}
// Turns a number into a string array
console.log(makeList(123));
// Turns a string into an array
console.log(makeList("hello"));
// Turns an object dictionary into an array
console.log(makeList({ a: "A", b: "B" }));
好的,很简单,而且效果很好。这条线对类型安全至关重要:
if (typeof input === "string" || typeof input === "number") {
它确保了中的只在对象列表
上运行,而TypeScript编译器为我们做了大量的工作!这里的缺点是这行代码有多冗长,特别是如果您要添加更多类型,或者我们应该能够做到这一点:
if (['string', 'number'].includes(typeof input)) {
但是,这将导致TypeScript编译器错误,基本上表明for循环
不确定字符串
和数字
类型无法到达它
“for…in”语句的右侧必须为“any”类型、对象类型或类型参数,但此处的类型为“string | number | ObjectList”
下面是一个运行示例:
问题:为了更好地理解编译器,我对编译器为何表现出这种行为感兴趣。那么[].includes()
与编译器不兼容呢?静态分析是否太深?有没有我没有想到的运行时注意事项?当用于比较时,TS中的typeof
运算符是否还有一些附加的“魔力”?这在评论中提到或暗示过,但为了完整性,我想扩展成一个答案
当您直接检查输入的类型===“string”
时,编译器将其视为类型保护,这对输入的外观类型有影响。而在检查之前,输入
仅为其声明的类型(例如,ObjectList | string | number
),在检查之后,编译器可以在代码区域中为其提供更窄、更明显的类型,只有在检查返回true
(例如,string
)时才能达到该类型,在代码区域中的另一种明显类型,只有当检查返回false
(例如ObjectList | number
)时才能到达。这就是所谓的
通过模拟代码在所有可能输入上的行为并给出输入
所有可能值的并集,这种控制流分析不起作用。它也不能通过直觉和智力工作;编译器无法简单地“理解”当且仅当某些变量比它们声明的类型窄时,某些代码块是可访问的。取而代之的是,已经实现了一系列特定的启发式,对应于已知用作类型保护的不同的可识别和公共编码模式
将typeof someVariable==“string”
视为类型保护就是这样一种启发
但是[“string”].includes(typeof someVariable)
未被编译器视为类型保护;没有人实施过这样的事情
关于类似问题/建议的有趣讨论可以在以下位置找到:为什么TypeScript不将Array.includes()
或Array.indexOf()
视为类型保护
正如@RyanCavanaugh(微软TypeScript团队的开发负责人)所说:
[缺少类型保护]是指缺少功能实现的行为,该功能将导致其按照OP建议的方式运行。现有的收缩机制都不适用于此;我们讨论的可能是一千行新代码,以基于这些方法正确检测和缩小数组,以及每个程序所付出的性能代价,因为控制流图必须更精细,以设置任何方法调用所导致的可能缩小,还有一个bug跟踪和混乱跟踪,这是由于人们期望这些函数的非方法版本也以同样的方式运行
我想这个案子也是如此;在编译器中实现这种类型保护将花费大量的工作,使编译器对每个人都变慢,并引入更多的复杂性和可能的bug,所有这些都是为了支持一个相对不常见的用例
幸运的是,TypeScript确实让开发人员能够通过编写自己的自定义类型保护。您需要将预期的类型保护重构为一个作用于其参数之一的布尔返回函数,这意味着您仍然无法获得[“string”]。includes(typeof input)
,以实现开箱即用,但您至少可以更进一步
下面是一个可能的实现:
interface TypeofMap {
string: string;
number: number;
bigint: bigint;
boolean: boolean;
symbol: symbol;
undefined: undefined;
object: object | null;
function: Function
}
function typeofIncludes<K extends keyof TypeofMap>(typeofs: K[], val: any): val is TypeofMap[K];
function typeofIncludes(typeofs: Array<keyof TypeofMap>, val: any) {
return typeofs.includes(typeof val);
}
这很有效。在if(typeofIncludes([“string”,“number”],input))
的“then”子句中,input
已缩小为string | number
,而在“else”子句中,它已缩小为ObjectList
评论中提到或暗指了这一点,但为了完整起见,我想进一步给出答案
当您直接检查输入的类型===“string”
时,编译器将其视为类型保护,这对输入的外观类型有影响。而在检查之前,输入
仅为其声明的类型(例如,ObjectList | string | number
),在检查之后,编译器可以在代码区域中为其提供更窄、更明显的类型,只有在检查返回true
(例如,string
)时才能达到该类型,在代码区域中的另一种明显类型,只有当检查返回false
(例如ObjectList | number
)时才能到达。这就是所谓的
通过模拟代码在所有可能输入上的行为并给出输入
所有可能值的并集,这种控制流分析不起作用。它也不能通过直觉和智力工作;编译器无法简单地“理解”某些代码块是rea
function makeList(input: ObjectList | string | number): string[] {
if (typeofIncludes(["string", "number"], input)) {
return [String(input)];
}
const arr = [];
for (const x in input) {
arr.push(String(input[x]));
}
return arr;
}