TypeScript 的编译器无法从其实现中验证或推断函数输入和输出类型之间的条件关系。它可以在函数实现内部执行控制流分析,以根据类型保护测试缩小返回类型,但整个函数的返回类型仅根据所有返回类型的 union 推断或验证。
因此,编译器能够为您的预期功能实现找出最好的方法是:
function get(opts: Options & ({ type: "person" } | { type: "car" })): Person | Car {
switch (opts.type) {
case "person": return { name: "john" }
case "car": return { wheels: 4 }
}
}
返回类型为Person | Car。 opts.type 与返回的Person | Car 的特定成员之间的关系丢失。您可以使用重载告诉编译器有关关系,但这相当于type assertion。只有当您断言的关系与其可以验证的内容完全无关时,编译器才会抱怨:
function getOops(opts: Options & { type: "person" }): Person
function getOops(opts: Options & { type: "car" }): Car // error!
function getOops(opts: Options & ({ type: "person" } | { type: "car" })) {
switch (opts.type) {
case "person": return { name: "john" }
case "car": return { wheels: "round" } // this isn't a Car or a Person
}
}
因此重载不可靠,就像类型断言不可靠一样:您可以使用它们来欺骗编译器。
有一个(长期存在的)未解决的问题要求您进行类型检查:microsoft/TypeScript#10765。目前尚不清楚如何在不对性能产生非常负面影响的情况下改进这一点。此外,人们依赖于这种不健全性,并且经常使用重载签名作为类型断言的替代方案,因此修复此问题最终将成为大量代码的巨大突破性变化。在可预见的未来,您应该将重载函数语句实现视为需要由 implementer 而不是由 compiler 保证类型安全的地方。
除此之外:重载是 TypeScript 的一项旧功能,可能会被 generics 和 conditional types 取代。您可以使用签名<T extends string | number>(a: T) => T extends string ? number : string; 而不是{ (a: string) => number; (a: number) => string; }。但是泛型条件类型与重载有几乎相同的问题:编译器无法验证实现中的关系。唯一的区别是,对于条件类型,编译器总是抱怨,你需要一个类型断言,而对于重载,编译器基本上是沉默的:
function getGenericConditional<K extends "person" | "car">(
opts: Options & { type: K }
): K extends "person" ? Person : Car {
switch (opts.type) {
// need to assert both of these
case "person": return { name: "john" } as K extends "person" ? Person : Car
case "car": return { wheels: 4 } as K extends "person" ? Person : Car
}
throw new Error(); // compiler can't tell that this is exhaustive
}
这里还有一个未解决的问题要求对此进行改进:microsoft/TypeScript#33912,同样,尚不清楚如何有效地做到这一点。
那么,还有其他选择吗?编译器能够验证的一件事是,如果您有一个T 类型的值和一个扩展keyof T 的K 类型的键,那么对该属性的索引会产生一个T[K] 类型的值。如果您可以使您的通用事物类似于键(例如"person" 和"car"),那么您可以将输入到输出的关系重新构建为索引访问:
type Mapping = {
person: Person,
car: Car
}
function getIndexed<K extends "person" | "car">(opts: { type: K }): Mapping[K] {
return ({ person: { name: "john" }, car: { name: "john" } })[opts.type]; // error!
}
如果您不想预先创建 Mapping 对象,您可以使用 getters 来推迟创建返回值,直到您需要它:
function getIndexedDeferred<K extends "person" | "car">(opts: { type: K }): Mapping[K] {
const map: Mapping = {
get person() { return { name: "john" } },
get car() { return { name: "john" } } // error!
}
return map[opts.type];
}
所以上面是类型安全的,编译器也知道,但它不是惯用的 JS,所以你可能更喜欢只处理你的重载。
好的,希望对您有所帮助;祝你好运!
Playground link to code