【问题标题】:Type is not assignable to conditional type类型不可分配给条件类型
【发布时间】:2020-01-02 10:40:42
【问题描述】:

我在操场上有一个 TypeScript 代码 sn-p。请查看TypeScript playground 或此处:

enum MyTypes {
    FIRST = "FIRST",
    SECOND = "SECOND",
    THIRD = "THIRD"
}

type TFirst = {
    type: MyTypes.FIRST
    foo: string
}

type TSecond = {
    type: MyTypes.SECOND
    foo: string
}

type TThird = {
    type: MyTypes.THIRD
    bar: string
}

type TConditionalType<T> =
    T extends MyTypes.FIRST ? TFirst :
    T extends MyTypes.SECOND ? TSecond :
    T extends MyTypes.THIRD ? TThird :
    null

const getMyObjectBasedOnType = <T extends MyTypes>(type: T): TConditionalType<T> | null => {
    switch (type) {
        case MyTypes.FIRST: {
            return {
                type: MyTypes.FIRST,
                foo: 'test'
            }
        }
        default: {
            return null
        }
    }
}

const firstObject = getMyObjectBasedOnType(MyTypes.FIRST)
// firstObject is type of TFirst or null which is okay
if (firstObject) {
    firstObject.foo
}

有一个函数getMyObjectBasedOnType(type: T),它根据type 参数返回一个条件类型的对象。这似乎有效,因为末尾的firstObjectTFirst | null 类型。一切都在这里。

当我返回对象时,我遇到的问题是第 31 行提到的函数中出现 TypeScript 错误。我明白了: Type '{ type: MyTypes.FIRST; foo: string; }' is not assignable to type 'TConditionalType&lt;T&gt;'. 我不知道出了什么问题。据我了解,这是TFirst 的对象,应该没问题。为什么我会收到此错误,什么是正确的解决方法?

【问题讨论】:

    标签: javascript typescript enums conditional-types


    【解决方案1】:

    关于您的问题,它来自延迟的条件类型。查看打字稿文档:https://www.typescriptlang.org/docs/handbook/advanced-types.html#conditional-types。 (搜索 conditional types are deferred 以到达页面中的正确位置)。

    对这个设计决策进行了一些简短的讨论: https://github.com/Microsoft/TypeScript/issues/29939.

    最简单的解决方案是使用更宽松的单独实现签名,同时将公共签名保留为对调用者更好的条件类型:

    type TConditionalType<T> =
        T extends MyTypes.FIRST ? TFirst :
        T extends MyTypes.SECOND ? TSecond :
        T extends MyTypes.THIRD ? TThird :
        null
    
    function getMyObjectBasedOnType<T extends MyTypes>(type: T): TConditionalType<T>; 
    function getMyObjectBasedOnType(type: MyTypes): TFirst | TSecond | TThird | null {
      switch (type) {
        case MyTypes.FIRST: {
          return {
            type: MyTypes.FIRST,
            foo: "test"
          }; // nothing wrong here
        }
        case MyTypes.SECOND: {
          return {
            type: MyTypes.FIRST,
            foo: "test"
          }; // unfortunately it would work... The implementation is permissive
        }
        default: {
          return null;
        }
      }
    }
    
    const firstObject = getMyObjectBasedOnType(MyTypes.FIRST)
    if (firstObject) {
        firstObject.foo; // it would work
        firstObject.bar; // it would fail
    
    }
    

    我仍在研究如何使它与箭头函数一起使用。要了解这两者之间的区别,您可以参考这里:Proper use of const for defining functions in JavaScript

    【讨论】:

    • 我知道这个解决方案,但它不是我想要的。正如我所说,我的解决方案非常适合firstObject。它是正确的TFirst,它不允许我使用firstObject.bar。我在case MyTypes.FIRST 内部有问题。我会说 TypeScript 应该知道该对象是 TFirstTConditionalType&lt;T&gt;。我可以制作类似 return { type: MyTypes.FIRST, foo: 'test' } as TConditionalType&lt;T&gt; 的东西,但为什么我需要告诉 TypeScript 对象是那种类型?
    • 如果您的函数在返回中使用条件类型,则需要使用类型断言,因为打字稿不会尝试推理条件类型。有两个讨论讨论您的案例:一个在这里:stackoverflow.com/questions/52817922/… 另一个在这里:stackoverflow.com/questions/50642020/…。据我所知,我不确定我们是否可以在这里做得更好,因为条件类型被设计推迟了......
    【解决方案2】:

    Pierre-Louis's solution 非常优雅且有文档记录?

    另一种更详细但仍然有效的替代方法:

    • 将类型 TConditionalType 包装在类型 Prettify 中,从而重构对象类型,从而强制 TypeScript 编译器不要“延迟”。
    • 使用类型断言

    ? 小建议:避免命名类型 T* (TFirst, TSecond, TThird, TConditionalType) 以区别于泛型类型约束 → 在下面的代码中,我分别将它们命名为FirstSecondThirdMyTypesMapped

    enum MyTypes {
        FIRST = "FIRST",
        SECOND = "SECOND",
        THIRD = "THIRD"
    }
    
    type First = {
        type: MyTypes.FIRST
        foo: string
    }
    
    type Second = {
        type: MyTypes.SECOND
        foo: string
    }
    
    type Third = {
        type: MyTypes.THIRD
        bar: string
    }
    
    type Prettify<T> =
        T extends infer Tbis ? { [K in keyof Tbis]: Tbis[K] } : never
    
    type MyTypesMappedTmp<T extends MyTypes> =
        T extends MyTypes.FIRST ? First :
        T extends MyTypes.SECOND ? Second :
        T extends MyTypes.THIRD ? Third :
        never
    
    type MyTypesMapped<T extends MyTypes> = Prettify<MyTypesMappedTmp<T>>
    
    const getMyObjectBasedOnType = <T extends MyTypes>(type: T) => {
        switch (type) {
            case MyTypes.FIRST:
                return {
                    type: MyTypes.FIRST,
                    foo: 'test'
                } as MyTypesMapped<T>
            // ...
            default:
                return null
        }
    }
    

    【讨论】:

    • 这并不能真正解决问题。我也可以这样做return { type: MyTypes.FIRST, foo: 'test' } as TConditionalType&lt;T&gt;。我想避免使用as 语法,因为它可能会产生误导。
    猜你喜欢
    • 2020-04-02
    • 2019-06-02
    • 2018-07-01
    • 2019-08-11
    • 2018-10-26
    • 2021-03-23
    • 2018-01-04
    • 2020-11-09
    • 1970-01-01
    相关资源
    最近更新 更多