Search code examples
typescripttypescript3.0

Typescript: mapped types, strip field from a union


I'm trying to create a type mapper NewRecord<T> which will strip the id type out of <T>. That's how I do it:

type NewRecord<T> = {
  [P in Exclude<keyof T, 'id'>]: T[P]
}

but, unfortunately, it doesn't play nice with union types. Let me illustrate:

interface IRecord {
  id: number
}

interface IBotRecord extends IRecord {
  isBot: true
  cpuCores: 4
}

interface IHumanRecord extends IRecord {
  isBot: false
  isHungry: true
}

type ICreature = IHumanRecord | IBotRecord

type INewBotRecord = NewRecord<IBotRecord>
type INewHumanRecord = NewRecord<IHumanRecord>
type INewCreature = NewRecord<ICreature>

const newHuman:INewHumanRecord = {
  isBot: false,
  isHungry: true // works!
}

const newCreature:INewCreature = {
  isBot: false,
  isHungry: true // does not exist in type NewRecord<ICreature>
}

It happens because keyof iterates over intersection of types, not unions and that's intended behaviour: https://github.com/Microsoft/TypeScript/issues/12948

What is the correct way to strip a field from the union?


Solution

  • You want to apply the mapped type for each member of the union. Fortunately conditional types have this exact behavior, they distribute over naked type parameters. This means that the mapped type is applied independently to each member of the union and all results are unioned into the final type. See here and here for more explanations.

    In this case the condition in the conditional type can just be extends any we don't care about the conditional part we care just about the distribution behavior of the conditional type:

    type NewRecord<T> = T extends any ? {
        [P in Exclude<keyof T, 'id'>]: T[P]
    } : never
    
    type INewCreature = NewRecord<ICreature>
    // The above type is equivalent to
    type INewCreature = {
        isBot: false;
        isHungry: true;
    } | {
        isBot: true;
        cpuCores: 4;
    }
    const newCreature:INewCreature = {
      isBot: false,
      isHungry: true // works fine
    }