Reputation: 1576
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?
Upvotes: 1
Views: 166
Reputation: 249606
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
}
Upvotes: 4