繁体   English   中英

如何在 typescript 中将可区分联合与 function 重载相结合

[英]How to combine discriminated unions with function overloads in typescript

我有一个非常简单的示例,我希望 typescript 通知我返回类型错误:

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"})

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

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

因此,编译器能够为您的预期 function 实现找出最好的方法是:

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


相关方面:重载是 TypeScript 的旧功能,可以用generics条件类型替换。 而不是{ (a: string) => number; (a: number) => string; } { (a: string) => number; (a: number) => string; } { (a: string) => number; (a: number) => string; } ,你可以有签名<T extends string | number>(a: T) => T extends string? number: string; <T extends string | number>(a: T) => T extends string? 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" ),那么您可以将输入到 output 的关系重新构建为索引访问:

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 object,您可以使用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,所以你可能更喜欢只注意你的重载。


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

Playground 代码链接

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM