Type narrowing of "one level deep" discriminated union in Typescript

Viewed 287

I'm not able to get the following to typecheck:

type Apple = {
  variety: {type: 'fuji'} | {type: 'gala'}
}

type FujiApple = {
  [P in keyof Apple]: P extends 'variety'
    ? Extract<Apple['variety'], {type: 'fuji'}>
    : Apple[P]
}

function fujiFn(fugi: FujiApple) {}

function appleFn(apple: Apple) {
  if (apple.variety.type === 'fuji') {

    // FAILS: Argument of type 'Apple' is not assignable to parameter of type
    // 'FujiApple'
    fujiFn(apple)
  }
}

Is there a way to type this to get the narrowing to work as intended? This happens only when the discriminator is not at the top level.

Solution I don't Like:

The following actually works, but it's... ugly and I would rather not do this.

fujiFn({...apple, variety:apple.variety})
2 Answers

You can use typeguards:

function isFujiApple(arg: Apple): arg is FujiApple {
  return arg.variety.type === 'fuji';
}

function appleFn(apple: Apple) {
  if (isFujiApple(apple)) {
    fujiFn(apple); // Now it works
  }
}

I believe that there is an issue in TS , because it is unable to infer nested properties of unions.

Here is the solution:


type Fuji = { type: 'fuji' }
type Gala = { type: 'gala' }
type Variety = Fuji | Gala

type Apple = {
  variety: Variety
}

type FujiApple = {
  [P in keyof Apple]: P extends 'variety'
  ? Extract<Apple['variety'], { type: 'fuji' }>
  : Apple[P]
}


function fujiFn(fugi: FujiApple) { }


function appleFn(apple: Variety) {
  if (apple.type === 'fuji') {
    fujiFn({ variety: apple }) // ok
  }
}
Related