Let's say I have these types:
type BaseAnimal = {
species: string
owner: boolean
}
type Cat = BaseAnimal & {
species: 'cat'
hasTail: boolean
}
type Dog = BaseAnimal & {
species: 'dog'
likesWalks: boolean
}
type Animal = Cat | Dog
And I want to create a type called AnimalParams
, which is identical to Animal
except the owner
property, which is a string.
I can't do either of the below.
// This seems to keep the owner property from Animal instead of overwriting
// So it raises an error if I try to specify owner as a string
type AnimalParams = Animal & {
owner: string
}
// This strips away properties unique to Cat or Dog
// So it raises an error if I try to specify hasTail or likesWalks
type AnimalParams = Omit<Animal, 'owner'> & {
owner: string
}
Now, the only workaround I can think of is to do as below, but this seems unnecessarily repetitive. Is there a cleaner, more concise way?
type CatParams = Omit<Cat, 'owner'> & {
owner: string
}
type DogParams = Omit<Dog, 'owner'> & {
owner: string
}
type AnimalParams = CatParams | DogParams
I read a few SO threads on utility types (such as Overriding interface property type defined in Typescript d.ts file, which was for interfaces), but couldn't find what I needed. Thanks for any answers in advance!
Instead of manually omitting owner
prop from each type, you can use distributive conditional type:
type OmitOwner<T = Animal> = T extends BaseAnimal ? Omit<T, 'owner'> : never;
type AnimalParams = OmitOwner & {
owner: string
};
Which is equivalent to:
(Omit<Cat, 'owner'> & { owner: string; })
| (Omit<Dog, 'owner'> & { owner: string; })
That's due to automatic distribution over union types
Instantiation of
T extends U ? X : Y
with the type argumentA | B | C
forT
is resolved as(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)
keyof
union produces intersection of keys of types in union, so
type AnimalKeys = keyof Animal // is "species" | "owner"
And implementation of Omit
is:
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;