What is the procedure for updating a property within an intersection type in Typescript?

Consider the following 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

Now, let's say we need a type called AnimalParams. This type should be similar to Animal but with a different type for the owner property, which should be a string.

There are limitations to achieving this directly as shown below:

// This approach retains the original owner property instead of replacing it
// Therefore, specifying owner as a string results in an error
type AnimalParams = Animal & {
  owner: string
}

// This method removes unique properties from Cat or Dog types
// As a result, trying to specify hasTail or likesWalks leads to errors
type AnimalParams = Omit<Animal, 'owner'> & {
  owner: string
}

One way to work around this limitation is by creating separate types for each animal as shown below. However, this solution appears repetitive. Is there a more concise and cleaner approach?

type CatParams = Omit<Cat, 'owner'> & {
  owner: string
}

type DogParams = Omit<Dog, 'owner'> & {
  owner: string
}

type AnimalParams = CatParams | DogParams

I've explored some Stack Overflow threads on utility types but couldn't find a suitable solution yet. Any suggestions are appreciated. Thank you!

Answer №1

If you're tired of tediously excluding the owner prop from every type, consider utilizing a distributive conditional type:

type OmitOwner<T = Animal> = T extends BaseAnimal ? Omit<T, 'owner'> : never;

type AnimalParams = OmitOwner & {
  owner: string
};

This approach is essentially equal to:

(Omit<Cat, 'owner'> & { owner: string; }) 
  | (Omit<Dog, 'owner'> & { owner: string; })

The magic lies in the automatic distribution across union types

When T extends U ? X : Y is instantiated with the type argument A | B | C for T, it resolves as

(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)

Try it out on the Playground


Why did the original approach fail?

The keyof union results in an intersection of keys from the types within the union, hence:

type AnimalKeys = keyof Animal // will be "species" | "owner"

Additionally, the implementation of Omit goes like this:

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

Answer №2

In my experience, I've found this technique to be quite effective for overriding props in React:

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

export type CustomAvatarProps = Extend<AvatarProps, {
    /** Function triggered when avatar is clicked. */
    onClick: () => void;
    /** Size of the avatar */
    size: number;
}>

It's possible to create concise one-liners for exports using this method.

Answer №3

For those who prefer sticking with types over interfaces, there is a way to minimize redundancy using generics:

type BasePetParams<P extends BasePet> = Omit<P, 'owner'> & {
    owner: string;
}

type PetParams = BasePetParams<Dog> | BasePetParams<Cat>;

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Unable to set up enzyme adapter

Currently, I am in the process of setting up the enzyme adapter for testing purposes. The code snippet that I have is quite straightforward: import * as enzyme from 'enzyme'; import * as Adapter from 'enzyme-adapter-react-16'; enzyme. ...

I'm encountering issues with undefined parameters in my component while using generateStaticParams in Next.js 13. What is the correct way to pass them

Hey there, I'm currently utilizing the App router from nextjs 13 along with typescript. My aim is to create dynamic pages and generate their paths using generateStaticParams(). While the generateStaticParams() function appears to be functioning corre ...

Determine the type of function arguments based on provided hints in TypeScript

It's common to encounter situations like this where TypeScript struggles to infer types due to lack of context. Is there a way to explicitly declare the type of function for the compiler? router.get('/get', imget); router.get('/send&a ...

What is the best way to include a new property to an existing interface and then export the updated interface in Typescript?

Can you provide guidance on creating a new interface - UIInterface that combines SummaryInterface with additional properties? For example: import { SummaryInterface } from 'x-api'; // summaryInterface includes 20+ predefined properties generated ...

Extending an existing interface within a globally declared namespace in Typescript

My current challenge involves extending an existing interface within KendoUI that originates from a specific definition file. Typically, using interface merging makes this task simple, but the interface I want to extend exists in the unique global namespac ...

Discovering new bugs in VSCode Playwright Tests but failing to see any progress

This morning, everything was running smoothly with debugging tests. However, after a forced reboot, I encountered an issue where it seems like the debugger is running, but nothing actually happens. This has happened before, but usually resolves itself. Unf ...

Expo background fetch initialized but not activated

During the development of my React Native app, I encountered the need to perform periodic background fetches from another server. To achieve this, I utilized two classes from Expo: import * as BackgroundFetch from 'expo-background-fetch'; import ...

Exploring the automatic type inference functionality of Typescript for generic classes

Although it may seem simple, I am struggling to pinpoint the cause of this error. I have been searching for a solution for quite some time, but I have yet to find one. class MyClass<T > { property: T = 5 // Error Here: Type '5' is not as ...

How can I achieve a result using a floating label in a .ts file?

I'm facing a simple issue that I can't seem to figure out. The problem is with a floating label in my HTML file, as shown below: <ion-list> <ion-item> <ion-label floating >Username</ion-la ...

Issue with retrieving all phone numbers from a contact in Ionic

Hope you're doing well! I'm encountering an issue with Ionic Contacts. Currently, I'm able to retrieve all the contacts using my code but I need help extracting all the phone numbers associated with each contact. For example, if John has 3 ...

I am receiving a warning about Tailwind CSS in my project and I'm unsure how to proceed

After moving on from a live project built with Angular, I revisited the project only to encounter a warning related to Tailwind CSS in my styles. It seems like there is an issue with importing statements conflicting with other rules. Despite attempting var ...

Tips for updating state in React TypeScript 2.0?

Working with a small component built using React and TypeScript has presented a unique challenge. interface Props { } interface State { isOpen: boolean; } class App extends React.Component<Props, State> { constructor(props: Props) { super ...

Unable to find solutions for all parameters in AnalysisComponent: ([object Object], ?, ?, [

As a newcomer to the Angular framework, I encountered an issue when adding project services. Error: Can't resolve all parameters for AnalysisComponent: ([object Object], ?, ?, [object Object], [object Object], [object Object], [object Object], [obj ...

Tips for properly implementing an enum in TypeScript when using the React useState hook

What's the correct way to utilize my useState hook? I have this enum type: export enum Status { PENDING = 'pending', SUCCESS = 'success', ERROR = 'error', } And the useState hook: const [isValid, setIsValid] = use ...

Error Alert: The function split cannot be applied to params[item]

ERROR! The console is outputting a message stating that TypeError: params[item].split is not a function. I would greatly appreciate any assistance in resolving this issue. Understanding where I went wrong in tackling this problem would be the most benefici ...

How can you utilize Angular Signals in combination with HostBinding to dynamically update styles?

Within a component called app-test, the following code is present: @Input({ transform: booleanAttribute }) reverse: boolean = false; @HostBinding('style.flex-direction') direction: string = this.reverse ? 'column-reverse' : &ap ...

The `setState` function is failing to change the current value

I'm having an issue with setting State in the dropdown component of semantic-ui-react while using TypeScript in my code. The selected category value is always returning an empty string "". Any suggestions on how to resolve this problem? impo ...

What is the best way to refine object T types by supplying an array of exclusions (keyof T)[]?

What's the best way to create a type guard that can exclude keys from an object in TypeScript? Below is my getExcludedKeys function, which aims to filter out certain keys from an object. However, I'm encountering an issue where the type guard is ...

How can I adjust the column width in OfficeGen?

Currently, I am utilizing officeGen for the purpose of generating word documents. <sup> let table = [ [ { val: "TT", fontFamily: "Times New Roman", }, { val: "Ten hang", ...

bespoke session with Next.js using Next-Auth

I encountered an issue while migrating my JS file to TSX. What I am trying to do is sign in with credentials and customize the session user to my user data. // api/auth/[...nextauth].js import NextAuth from "next-auth"; import Providers from &qu ...