How to overwrite property for intersection type in Typescript?
Asked Answered
E

3

18

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!

Erythrocytometer answered 16/7, 2020 at 6:10 Comment(0)
N
16

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>>;
Negron answered 16/7, 2020 at 6:37 Comment(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)".Erythrocytometer
G
8

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.

Give answered 12/7, 2022 at 12:54 Comment(0)
B
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>;
Bryson answered 16/7, 2020 at 6:22 Comment(3)
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.Erythrocytometer
Yeah, totally agree. Hadn't thought of using distributive conditional types.Bryson
I like this solution because it can also be nicely generalized, see e.g. Modify at https://mcmap.net/q/73697/-overriding-interface-property-type-defined-in-typescript-d-ts-fileFloatfeed

© 2022 - 2024 — McMap. All rights reserved.