如何在打字稿中将可区分的联合与函数重载相结合



>我有一个非常简单的例子,我希望打字稿通知我返回类型错误:

interface Options {
type: "person" | "car"
}
interface Person {
name: string
}
interface Car {
wheels: number
}
function get(opts: Options & {type: "person"}): Person
function get(opts: Options & {type: "car"}): Car
function get(opts: any): any {
switch (opts.type) {
case "person": return { name: "john" }
case "car": return {name: "john"} // why there is no error?
}
}
let person = get({ type: "person" })
let car = get({type: "car"})

我想我可能没有正确使用歧视工会。将判别联合与函数重载相结合的正确方法应该是什么?

TypeScript 的编译器无法从其实现中验证或推断函数输入和输出类型之间的条件关系。 它可以在函数实现内部执行控制流分析,以根据类型防护测试缩小返回类型,但整个函数的返回类型仅推断为所有返回类型的联合或进行验证。

因此,编译器能够为预期的函数实现找出的最佳方法是:

function get(opts: Options & ({ type: "person" } | { type: "car" })): Person | Car {
switch (opts.type) {
case "person": return { name: "john" }
case "car": return { wheels: 4 }
}
}

返回类型为Person | Caropts.type与返回Person | Car的特定成员之间的关系将丢失。 可以使用重载告知编译器有关关系的信息,但这等效于类型断言。 只有当你断言的关系与它可以验证的内容完全无关时,编译器才会抱怨:

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。目前尚不清楚如何在不对性能产生非常负面影响的情况下改进这一点。 此外,人们依赖于这种不健全性,并且经常使用重载签名作为类型断言的替代方法,因此修复此问题最终将成为大量代码的巨大突破性更改。 在可预见的未来,您应该将重载函数语句实现视为需要由实现者而不是编译器来保证类型安全的地方。


相关的是:重载是TypeScript的一个较旧的功能,可以用泛型和条件类型代替。 您可以拥有签名<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 TK类型的键,则索引到该属性会产生一个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对象,你可以使用 getter 来推迟创建返回值,直到你需要它:

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,所以你可能更愿意只注意你的重载。


好的,希望有帮助;祝你好运!

操场链接到代码

最新更新