15

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!

reesaspieces
  • 1,600
  • 4
  • 18
  • 47

3 Answers3

14

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 argument A | B | C for T is resolved as (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)

Playground


Why original attempt doesn't work?

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>>;
Aleksey L.
  • 35,047
  • 10
  • 74
  • 84
  • 1
    This was eye-opening. Thanks so much! For anyone referencing this question - I recommend that also you read the linked docs. Basically, "an instantiation of `T extends U ? X : Y `with the type argument `A | B | C` for `T` is resolved as `(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)`". – reesaspieces Jul 16 '20 at 09:00
6

If you really want to stick to types instead of interfaces, you could use generics to avoid the repitition:

type BaseAnimalParams<T extends BaseAnimal> = Omit<T, 'owner'> & {
    owner: string;
}

type AnimalParams = BaseAnimalParams<Dog> | BaseAnimalParams<Cat>;
Robby Cornelissen
  • 91,784
  • 22
  • 134
  • 156
  • Thank you! I'm accepting the other answer because it's more DRY (avoids manually creating another union type), but you also have my upvote. – reesaspieces Jul 16 '20 at 08:57
  • 1
    Yeah, totally agree. Hadn't thought of using distributive conditional types. – Robby Cornelissen Jul 16 '20 at 08:57
  • 1
    I like this solution because it can also be nicely generalized, see e.g. `Modify` at https://stackoverflow.com/a/55032655/3779853 – phil294 May 26 '22 at 22:50
5

I have been using this generic for overwriting props in react with success:

type Overwrite<T, NewT> = Omit<T, keyof NewT> & NewT;

export type TouchableAvatarPropTypes = Overwrite<AvatarPropTypes, {
    /** Function executed when avatar is pressed. */
    onPress: () => void;
    /** Size of the avatar */
    size: number;
}>

You can do some pretty clean one-liners for exporting.

HeadJk
  • 96
  • 1
  • 3