5

I have a TypeScript code snippet in the playground. Please take a look there at TypeScript playground or here:

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
}

There is a function getMyObjectBasedOnType(type: T) which returns an object of a conditional type based on the type parameter. This seems to work since firstObject at the end is of type TFirst | null. All clear here.

Problem which I have is TypeScript error inside mentioned function on line 31 when I am returning object. I get this: Type '{ type: MyTypes.FIRST; foo: string; }' is not assignable to type 'TConditionalType<T>'. I can't figure it out what is wrong. As far as I understand it that is an object of TFirst which should be okay. Why do I receive this error and what is proper fix for it?

Marek
  • 2,608
  • 4
  • 25
  • 32

2 Answers2

3

Regarding your issue it comes from conditional types that are deferred. Look at the typescript documentation: https://www.typescriptlang.org/docs/handbook/advanced-types.html#conditional-types. (search for conditional types are deferred to get to the right place in the page).

There were some brief discussions about this design decision: https://github.com/Microsoft/TypeScript/issues/29939.

The simplest solution is to use a separate implementation signature that is more permissive, while keeping the public signature with conditional types that is better for the caller:

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

}

I'm still figuring out how to make it works with arrow functions. To know the difference between those two you can refer here: Proper use of const for defining functions in JavaScript

  • I know this solution, but it is not what I am looking for. As I said my solution works perfectly for `firstObject`. It is correctly `TFirst` and it doesn't allow me to use `firstObject.bar`. I have a problem inside `case MyTypes.FIRST`. I would say that TypeScript should know that object is `TFirst` or `TConditionalType`. I can make something like this `return { type: MyTypes.FIRST, foo: 'test' } as TConditionalType`, but why do I need to tell TypeScript that object is that type? – Marek Sep 02 '19 at 10:43
  • If your function uses conditional types in the return it will need to use type assertions, as typescript will not try to reason about the conditional type. There are two discussions talking about your case: one here: https://stackoverflow.com/questions/52817922/typescript-return-type-depending-on-parameter there other one here: https://stackoverflow.com/questions/50642020/typescript-how-to-write-a-function-with-conditional-return-type. From what I see, I'm not sure that we can do much better here as conditional types are deferred by design... – Pierre-Louis Lacorte Sep 02 '19 at 15:59
0

Pierre-Louis's solution is very elegant and documented

An alternative, more verbose but still working:

  • Wrapping type TConditionalType in the type Prettify which reconstruct the object type, forcing the TypeScript compiler not to "defer".
  • Use type assertions

Little advice: avoid naming types T* (TFirst, TSecond, TThird, TConditionalType) to differentiate them from generic type constraint → In the code beneath, I've named them respectively First, Second, Third, MyTypesMapped:

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
    }
}
Romain Deneau
  • 2,841
  • 12
  • 24
  • 1
    This doesn't really solve asked question. I can do this as well `return { type: MyTypes.FIRST, foo: 'test' } as TConditionalType`. I would like to avoid using `as` syntax because it could be misleading. – Marek Sep 02 '19 at 10:13