Search code examples
typescriptstring-literalsdiscriminated-union

Type annotations and discriminated union's of string literals


I've been having this issue with creating objects that conform to a type annotation (Cards in the example below), with a property that is of the type of a discriminated union of string literals (CardType in the example below), these string literals are the exact same string literals as on the original type annotation.

// ------------ SETUP ------------
interface AboutCard {
  type: 'About'
}

interface SplashCard {
  type: 'Splash'
}

export type Cards = AboutCard | SplashCard
export type CardType = 'About' | 'Splash'

const type = 'About' as CardType
// ------------ SETUP END ------------

const example1: Cards = {
  type
}

// ^
// Type 'CardType' is not assignable to type '"Splash"'.
// Type '"About"' is not assignable to type '"Splash"'.ts(2322)

const example2 = {
  type
} as Cards

// ^ all good, no error.

const example3 = {
  type: 'NOT_REAL_CARD'
} as Cards

// ^
// Types of property 'type' are incompatible.
// Type '"NOT_REAL_CARD"' is not comparable to type '"Splash"'.ts(2352)

So basically I'm wondering why that first example:

const example1: Cards = {
  type
}

is failing, but the last two examples are doing what I would expect.

example1 seems to conform to the type Cards, and if you try explicitly setting type to either the string literal About or Splash it works fine, it's just when it can be the discriminated union that it has issues.

Sorry I don't know how to ask this better!

I feel like maybe this: why is TypeScript converting string literal union type to string when assigning in object literal? offers some clues as to why this is happening?


Solution

  • Except for trivial cases where there are no other properties, { type: 'About' | 'Splash' } is not the same as { type: 'About' } | { type: 'Splash' }. Since your variable type is of the union of all possible discriminants, you are trying to do exactly this, assign an object with key type that is of a union property to where a discriminated union is expected.

    The reason the latter two examples work, is you can do pretty much anything with a type assertion. You are effectively telling the compiler to suppress the type error it thinks is there.

    To understand why these types are not compatible consider this example:

    interface AboutCard {
        type: 'About'
        aboutMessage: string
    }
    
    interface SplashCard {
        type: 'Splash'
        spashMessage: string
    }
    
    export type Cards = AboutCard | SplashCard
    export type CardType = Cards['type'] // no need to repeat string liteal types
    
    const type = 'About' as CardType
    
    // What properties should be required for `example1` ? 
    // since it is both types at the same time best case scenario would be to require all properties in teh union
    const example1: Cards = {
        type,
        spashMessage: ""
    }