类型不可分配给条件类型。

5

我在 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<T>'. 我弄不清楚错在哪里。据我所知,那是TFirst的一个对象,应该没问题。为什么我会收到这个错误,如何正确解决?

2个回答

3
关于你的问题,它源自被推迟的条件类型。请参考typescript文档: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

}

我还在试图弄清楚如何使用箭头函数使其正常工作。要了解这两者之间的区别,您可以参考这里:JavaScript中定义函数的const的正确用法。最初的回答。

我知道这个解决方案,但它不是我要找的。正如我所说,我的解决方案对于firstObject完美地工作。它正确地是TFirst,并且不允许我使用firstObject.bar。我在case MyTypes.FIRST内部有一个问题。我认为TypeScript应该知道对象是TFirstTConditionalType<T>。我可以做这样的事情return { type: MyTypes.FIRST, foo: 'test' } as TConditionalType<T>,但为什么我需要告诉TypeScript对象是那种类型呢? - Marek
如果您的函数在返回值中使用条件类型,它将需要使用类型断言,因为TypeScript不会尝试推理条件类型。有两个讨论涉及到您的情况:一个在这里:https://dev59.com/P1QJ5IYBdhLWcg3w66Y5,另一个在这里:https://dev59.com/ZVUL5IYBdhLWcg3wOl9x。从我所看到的来看,我不确定我们能做得更好,因为条件类型是按设计推迟的... - Pierre-Louis Lacorte

0

Pierre-Louis的解决方案非常优雅且有文档记录

另一种更冗长但仍然有效的替代方法:

  • 将类型TConditionalType包装在类型Prettify中,重建对象类型,强制TypeScript编译器不要 "defer"。
  • 使用类型断言

小建议:避免将类型命名为T*TFirstTSecondTThirdTConditionalType)以将它们与通用类型约束区分开来 → 在下面的代码中,我将它们分别命名为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
    }
}

1
这并没有真正解决问题。我也可以这样做 return { type: MyTypes.FIRST, foo: 'test' } as TConditionalType<T>。不过,我想避免使用 as 语法,因为它可能会引起误解。 - Marek

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接