如何使TS中的工作成为可以基于其泛型参数(在构造函数中设置)返回不同类型的泛型类?

type Type = 'foo' | 'bar';

interface Res {
    'foo': {foo: number};
    'bar': {bar: string};
}

class MyClass<T extends Type> {
    constructor(readonly type: T) {}

    public run(): Res[T] {
        if (is(this, 'foo')) {
            return { foo: 123 };
        }
        return { bar: 'xyz' };
    }
}

function is<T extends Type>(instance: MyClass<Type>, type: T): instance is MyClass<T> {
    return instance.type === type;
}

由于可以在Playground中进行判断,因此本例给出以下错误:

类型‘{foo:number;}’不可赋值给类型‘res[T]’. 类型‘{foo:number;}’不可赋值给类型‘{foo:number;}&amp;{bar:string;}’. 类型‘{foo:number;}’中缺少属性‘bar’,但类型‘{bar:string;}’中需要属性‘bar’.

但常识(和类型保护)表明,通用T显然是正确的类型……那么,为什么它不适用于回程线路呢?

推荐答案

目前,genericsnarrowing在打印脚本中并不能很好地协同工作.当您选中is(this, 'foo')时,type guard function可以将外观类型this缩小到this & MyClass<"foo">,但它对泛型类型参数T没有任何影响.你可能会期望或希望T会从Type降到"foo",但事实并非如此.这意味着{foo: 123}不能被赋值给Res[T],你会得到一个错误.

请注意,虽然this.type === "foo"应该意味着T等于"foo"似乎是"显而易见的",但在一般情况下这样做实际上是不正确的.这是因为T might是由完整的union type "foo" | "bar"指定的,而建立this.type === "foo"并不能消除这种可能性.充其量你可以说你知道T"foo"一定有一些重叠,所以T & "foo"不是never,但这是一种很难实施或表达的约束.

有几个开放的功能请求要求通过控制流分析重新约束泛型类型参数.与此代码示例最相关的是microsoft/TypeScript#27808microsoft/TypeScript#33014,前者允许您强制T仅为one of"foo""bar",而不是联合;后者将使编译器看到,即使T是完整的联合,返回{foo: 123}也是安全的.这两种方法中的任何一种都可能使代码按原样工作,但目前它们不是语言的一部分,您需要解决它.


最简单的解决方法是使用type assertions来告诉编译器不要担心验证类型.例如:

public run(): Res[T] {
    if (is(this, 'foo')) {
        return { foo: 123 } as Res[T]; // okay
    }
    return { bar: 'xyz' } as Res[T]; // okay
}

对于您来说,这是最便捷的方式,只需对代码进行最少的更改.但请注意,在这里,您正在将类型判断的责任从编译器手中移开.您可以将上面的判断更改为(!is(this, 'foo')),编译器仍然会对它感到满意.因此,在进行类型断言以证明您所说的是事实时,您必须小心.


如果这里更重要的是从编译器获得一些类型安全保证,那么解决方法将涉及重构,从类型保护和控制流分析转向泛型indexing.您想要的返回类型是indexed access type Res[T],因此如果您希望编译器理解这一点,您可以使用类型T的键索引到类型Res的对象,并返回该对象:

public run(): Res[T] {
    return {
        foo: { foo: 123 },
        bar: { bar: 'xyz' }
    }[this.type]; // okay
} 

这可能看起来很奇怪,但它类似于判断this.type,然后根据结果 bootstrap 控制流的逻辑.这里唯一的主要区别是运行库实际上将在每次调用run()时同时计算可能的返回值{foo: 123}{bar: 'xyz'},即使它丢弃了其中的一个.对于没有这种副作用的小对象文字来说,这可能无关紧要.但是,如果您确实希望拥有与前面相同的行为,则可以使用getters来确保只执行与实际this.type值对应的代码块:

public run(): Res[T] {
    return {
        get foo() { return { foo: 123 } },
        get bar() { return { bar: 'xyz' } }
    }[this.type]; // okay
}

现在idthis.type"foo",return {foo: 123}将运行,而return {bar: 'xyz'}不会.

您可以修改此实现以执行其他操作,但基本方法是通过实际的索引操作实现索引访问类型,而不是通过if/else语句.


这就对了.该语言还没有准备好同时使用泛型和类型保护,您可以通过放松类型判断或使用不带类型保护的泛型来解决这个问题.

Playground link to code

Typescript相关问答推荐

类型断言添加到类型而不是替换

如何使类型只能有来自另一个类型的键,但键可以有一个新值,新键应该抛出一个错误

将props传递给子组件并有条件地呈现它''

`((PrevState:NULL)=>;NULL)|null`是什么意思?

如何推断哪个特定密钥与泛型匹配?

使某些(嵌套)属性成为可选属性

无法从Chrome清除还原的初始状态数据

如何在另一个参数类型中获取一个参数字段的类型?

使用TypeScrip根据(可选)属性推断结果类型

被推断为原始类型的交集的扩充类型的交集

类型TTextKey不能用于索引类型 ;TOption

在HighChats列时间序列图中,十字准线未按预期工作

与toast.error一起react 中的Async TUNK错误消息

从类型脚本中元组的尾部获取第n个类型

类型实例化过深,可能是无限的&TypeScrip中的TupleUnion错误

在构建Angular 应用程序时,有没有办法通过CLI传递参数?

如何使用警告实现`RecursiveReadonly<;T>;`,如果`T`是原语,则返回`T`而不是`RecursiveReadonly<;T>;`

如何在TypeScrip中使用数组作为字符串的封闭列表?

如何在没有空接口的情况下实现求和类型功能?

从平面配置推断嵌套递归类型