Determining the property type in Typescript based on the value of another property

Unique Code

interface Order {
    customer: Customer,
    address: Address
}

interface Customer {
    name: string
}

interface Address {
    firstLine: string
}

interface OrderUpdateRequest {
    key: 'customer'|'address',
    value: Customer | Address
}

const myThing = {customer: {name: 'Alice'}, address: {firstLine: '123 Main St'}} as Order

const updateRequest = {key:'customer',  value: {name : 'Eve'}} as OrderUpdateRequest
myThing[updateRequest['key']] = updateRequest['value']

Description

Check out the Playground

Based on the playground above, I aim to develop a versatile interface that can accept either customer or address as a key. Additionally, TypeScript should recognize that if the key is customer, the value would be an instance of Customer.

Issue at Hand

I am encountering difficulties modifying the customer field on the order due to TypeScript requiring the value of the request to adhere to both Customer AND Address interfaces

Type 'Customer | Address' is not assignable to type 'Customer & Address'.
  Type 'Customer' is not assignable to type 'Customer & Address'.
    Property 'firstLine' is missing in type 'Customer' but required in type 'Address'.(2322)
input.tsx(11, 5): 'firstLine' is declared here.

Please bear in mind that despite similarities in wording with another issue, the scenario and resolution are distinct. Check out this related thread for more information.

Update: Playground 2 Solution Playground

Answer №1

updateRequest is of type OrderUpdateRequest. Therefore, TypeScript will always assume that the value of the key property within updateRequest will be either

"customer" | "address"

If you wish to specify that updateRequest.key is "customer", you must add a generic type to the OrderUpdateRequest

interface OrderUpdateRequest<T extends "customer" | "address"> {
    key: T,
    value: T extends "customer" ?  Customer : Address
}

const myThing = {customer: {name: 'Bob'}, address: {firstLine: 'my street'}} as Order

const updateRequest = {key:'customer',  value: {name : 'Dave'}} as OrderUpdateRequest<"customer">
myThing[updateRequest.key] = updateRequest['value']

This example is quite straightforward and can be enhanced if you wish to include other possible keys. The main idea here is to showcase that:

Once a value is typed, TypeScript will only recognize the value based on its type, regardless of how the type was created. Hence, to introduce different behaviors based on the value of a property, generic typing should be utilized

If you need to incorporate more than two keys, you can utilize a mapping interface to link each key with a specific type. Then, utilize the key from your interface in your generic types

interface OrderUpdateRequestMapping {
    customer: Customer,
    address: Address
}

interface OrderUpdateRequest<K extends keyof OrderUpdateRequestMapping> {
    key: K,
    value: OrderUpdateRequestMapping[K]
}

Answer №2

If we explore another route, we could develop a function that produces an object of the type UpdateRequest<K, V> and then create overloads for that function:

// include additional key type variations here
type UpdateValue = User | Product;
type UpdateKey = "user" | "product";

type User = { username: string };
type Product = { description: string };

// employ generics to enhance the type's broad applicability
interface UpdateRequest<K extends UpdateKey, V extends UpdateValue> {
  key: K;
  value: V;
}

// function overloads 
export function generateUpdate(
  key: "user",
  value: User,
): UpdateRequest<"user", User>;

export function generateUpdate(
  key: "product",
  value: Product,
): UpdateRequest<"product", Product>;

// function implementation
export function generateUpdate<
  K extends UpdateKey,
  V extends UpdateValue,
>(key: K, value: UpdateValue) {
  return { key, value } as UpdateRequest<K, V>;
}

Personally, I find this method preferable due to the enhanced flexibility it affords, such as the exclusion of a ternary statement. Here is the outcome of the aforementioned code:

// yields -> UpdateRequest<"product", Product>
const x = generateUpdate("product", { description: "123" });

// yields -> UpdateRequest<"user", User>
const y = generateUpdate("user", { username: "Alice" }); 

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

The specified module '...' is identified as a non-module entity and therefore cannot be imported using this specific construct

Currently, I am facing an issue in my .tsx file where I am attempting to import a RaisedButton component from material-ui using the following code: import * as RaisedButton from 'material-ui/lib/raised-button' Unfortunately, this is triggering ...

Limit the types of components allowed as children in a React component by utilizing TypeScript

I've been struggling with this question for a while, searching through answers but still unable to get my code to work. Here's the issue at hand. Within my code, I have a component called Steps which acts as a wrapper. I also have multiple Step ...

After clicking on the "Delete Rows" button in the table, a white color suddenly fills the background in Angular Material

When the dialog box pops up, you'll see a white background color: https://i.stack.imgur.com/EflOx.png The TypeScript code for this action can be found in config-referrals.component.ts openDialog(action, obj) { this.globalService.configA ...

The JavaScript function for converting a date to a local string in the format of DD MMM YYYY is causing an error message in the browser console stating that it is not a valid function

I am encountering an issue with formatting a date string. The date is currently in the format 2021-03-31T00:00:00, and I need it to be displayed as 31 Mar 2021. In my TypeScript code, I attempted to use the following function: const formattedDate = i.Susp ...

Having trouble retrieving mobiscroll instance in Angular with Ionic

I'm facing an issue with accessing the instance of my mobiscroll widget in Angular 4 with Ionic Framework. Despite following all the correct steps, the actual widget won't get selected. Below is the code for the widget: <mbsc-widget [options ...

Troubleshooting TypeScript errors related to ReactNode post-upgrade to Create React App v5

Since upgrading to Create React App version 5, I've been encountering errors like the one below: TS2786: 'OutsideClickHandler' cannot be used as a JSX component. Its instance type 'OutsideClickHandler' is not a valid JSX element. ...

What is the best way to handle a ReadableStream for a POST request?

I'm currently working on implementing basic CRUD operations using the latest Next.js 13 route handlers in combination with Prisma using TypeScript. This is how my POST request handler appears: export async function POST(req: NextRequest) { const c ...

Struggling with the TypeScript generic syntax for the GroupBy function

Struggling to figure out where I'm going wrong with this TypeScript signature after spending some time on it. I've been working on a group by function: const group = <T>(items: T[], fn: (item: T) => T[keyof T]) => { return items.re ...

What is the best practice for Angular: running production build before or after testing?

When developing a Java application for production, I typically set up the build process to create the production artifacts first and then run tests against those artifacts. Recently, I joined an Angular project and noticed that the build process is struct ...

Autoplay halts on Ionic 3 image slider following manual slide navigation

My Ionic 3 image slider has autoplay functionality that works perfectly. However, I've noticed that when I manually slide the images, the autoplay feature stops working. I have included my Ionic 3 code below. I would greatly appreciate any help on thi ...

Localization of labels and buttons in Angular Owl Date Time Picker is not supported

When using the Owl Date Time Picker, I noticed that the From and To labels, as well as the Set and Cancel buttons are not being localized. Here is the code snippet I am using to specify the locale: constructor( private dateTimeAdapter: DateTimeAdapter&l ...

"What is the significance of the .default property in scss modules when used with typescript

When dealing with scss modules in a TypeScript environment, my modules are saved within a property named default. Button-styles.scss .button { background-color: black; } index.tsx import * as React from 'react'; import * as styles from ' ...

Utilizing the Double Mapping Feature in React with Typescript

It seems I might be overlooking something, can someone guide me on how to properly double map in this scenario? I'm encountering an error on the second map: Property 'map' does not exist on type '{ departure: { code: string; name: strin ...

What is the best way to access all the attributes (excluding methods) of an object in a class instance?

My goal is to generate a new object type that inherits all the properties from an existing class instance. In other words, I am looking for a way to transform a class instance into a plain object. For example, consider the following scenario: ...

Creating a Typescript HttpInterceptor and ensuring its compatibility with minification techniques

Currently, I am trying to implement an Angular HttpInterceptor based on the solution provided in this Stack Overflow response: However, I am struggling with the factory method: public static Factory($q: ng.IQService) { return new AuthenticationInter ...

Is there a way for me to indicate to my custom component the specific location within an input message where a value should be displayed

I'm currently developing a value selector component in ionic/angular and I've encountered an issue with the message/title that I want to pass to the component. I want to be able to specify where the selected value should appear within the message ...

Is it advisable to utilize TypeScript interfaces for declaration files and React component prop definitions?

Whenever I create structures for object types, my go-to method is to define them in my declaration.d.ts file like this: type TagsObject = { _id: string; tag: string; } type ProjectData = { _createdAt: string; _id: string; _rev: string; _type: ...

In what way can a piped operator in rxjs be influenced by the value returned by a subsequent operator?

When attempting to examine values between operators in an rxjs pipe, I decided to use tap to log them to the console. I added two taps, one before a map operator used for sorting an Array, and one after. Surprisingly, both taps logged the same sorted Arra ...

Webpack and TypeScript are throwing an error stating that `$styles` is not defined

I've encountered an issue with my typescript SharePoint spfx solution. After compiling using webpack, my $styles variable becomes undefined even though I am able to use the class names directly. It seems like there might be a configuration problem at ...

Establish a default value for a TypeScript generic Type: distinguishing between 'unknown' and 'any'

An interface has been created with a parameter that takes a generic type input named Data export interface MyStructure<Data> { id: string; data: Data; } The goal is to allow the Data type to be optional in order to support scenarios like: functi ...