Leveraging both the spread operator and optional fields can improve the productivity and readability of your

Imagine you have an object with a mandatory field that cannot be null:

interface MyTypeMandatory { 
    value: number;
}

Now, you want to update this object using fields from another object, but this time with an optional field:

interface MyTypeOptional { 
    value?: number;
}

So, you decide to create a function for this task:

function mergeObjects(a: MyTypeMandatory, b: MyTypeOptional) {
    return { ...a, ...b };
}

The question arises - what will be the expected return type of this function?

const result = mergeObjects({ value: 1 }, { value: undefined });

Experimenting reveals that it seems to adhere to the MyTypeMandatory interface, even when one of the spreads includes an optional field.

Interestingly, switching the order of the spreads doesn't affect the inferred type, despite potentially changing the actual runtime type.

function mergeObjects(a: MyTypeMandatory, b: MyTypeOptional) {
    return { ...b, ...a };
}

Why does TypeScript exhibit such behavior and what are some ways to navigate around this particular issue?

Answer №1

When dealing with object spread types, certain rules are applied as outlined in this guide:

Call and construct signatures are removed, only non-method properties are retained, and in cases where the same property name exists, the type of the rightmost property takes precedence.

With object literals containing generic spread expressions, intersection types are now generated, akin to the behavior of the Object.assign function and JSX literals. For instance:

Everything seems to work smoothly until an optional property is present in the rightmost argument. The ambiguity arises when dealing with a property like {value?: number; }, which could signify either a missing property or a property set to undefined. TypeScript struggles to differentiate between these two scenarios using the optional modifier notation ?. Let's consider an example:

const t1: { a: number } = { a: 3 }
const u1: { a?: string } = { a: undefined }

const spread1 = { ...u1 } // { a?: string | undefined; }
const spread2 = { ...t1, ...u1 } // { a: string | number; }
const spread3 = { ...u1, ...t1 } // { a: number; }
spread1

makes sense - the key a can be defined or left undefined. In this case, we must use the undefined type to represent the absence of a property value.

spread2

The type of a is dictated by the rightmost argument. If a is present in u1, it would be of type string; otherwise, the spread operation retrieves the a property from the first argument t1, which has a type of number. Thus, string | number is a reasonable outcome in this context. Note that there is no mention of undefined here because TypeScript assumes that the property does not exist at all or that it is a string. To observe a different result, we could assign an explicit property value type of undefined to a:

const u2 = { a: undefined }
const spread4 = { ...t1, ...u2 } // { a: undefined; }
spread3

In this scenario, the value of a from t1 replaces the value of a from u1, resulting in a return type of number.


I wouldn't anticipate an immediate resolution to this issue based on discussions. Therefore, a potential workaround involves introducing a distinct Spread type and function:

type Spread<L, R> = Pick<L, Exclude<keyof L, keyof R>> & R;

function spread<T, U>(a: T, b: U): Spread<T, U> {
    return { ...a, ...b };
}

const t1_: { a: number; b: string }
const t2_: { a?: number; b: string }
const u1_: { a: number; c: boolean }
const u2_: { a?: number; c: boolean }

const t1u2 = spread(t1_, u2_); // { b: string; a?: number | undefined; c: boolean; }
const t2u1 = spread(t2_, u1_); // { b: string; a: number; c: boolean; }

Hoping this provides some clarity! Check out this interactive demo for the above code.

Answer №2

Disclaimer: The following is my personal interpretation and theory about the subject, as it has not been confirmed.


An interesting issue arises when dealing with optional fields in TypeScript.

Consider defining a type like this:

interface MyTypeOptional { 
    value?: number;
}

In this case, TypeScript expects the value to be of type number | never when spreading the object.

However, due to the ambiguous nature of optional fields versus undefined values, TypeScript sometimes infers the spread object with an optional field type of number | undefined instead of number | never.

Strangely, when spreading a non-optional type alongside an optional type, TypeScript correctly casts the optional field into number | never.

To work around this inconsistency, it's advisable to avoid using optional fields until the TypeScript team resolves this issue. This can impact how you handle object keys being omitted if undefined, the usage of Partial types, and handling unset values within your application.

For more information on this limitation of optional fields in TypeScript, refer to the open issue here: https://github.com/microsoft/TypeScript/issues/13195

Answer №3

To address this issue, one possible solution is to utilize type level function as demonstrated below:

interface MyTypeRequired { 
    a: number;
    value: number;
}

interface MyTypeOptional { 
    b: string;
    value?: number;
}

type MergedWithOptional<T1, T2> = {
    [K in keyof T1]: K extends keyof T2 ? T2[K] extends undefined ? undefined : T2[K] : T1[K] 
} & {
    [K in Exclude<keyof T2, keyof T1>]: T2[K]
}

function createObject(a: MyTypeRequired, b: MyTypeOptional): MergedWithOptional<MyTypeRequired, MyTypeOptional> {
    return { ...b, ...a };
}

For testing purposes, additional fields have been included to observe the behavior of the solution. The essence lies in augmenting the result with T1[K] | undefined when encountering optional fields with possible undefined values in the second object. Meanwhile, the merger includes all other fields unique to T2 compared to T1.

Further explanations include:

  • K extends keyof T2 ? T2[K] extends undefined ? undefined : T2[K] : T1[K]
    conditions the addition of undefined to the value type if the key exists in the second object and can be undefined, demonstrating the behavior of conditional types for union types.
  • Exclude<keyof T2, keyof T1>
    - selects only the keys that are exclusive to the second object.

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

Calculate the variance in days between two dates and assign the result to a separate field

I am working with two date fields, one labeled Commencement Date and the other as Expiration Date. Objective: The goal is to calculate the difference in days between the selected commencement and expiration dates (expirationDate - commecementDate) and ...

What is the best way to invoke a method in a child component from its parent, before the child component has been rendered?

Within my application, I have a parent component and a child component responsible for adding and updating tiles using a pop-up component. The "Add" button is located in the parent component, while the update functionality is in the child component. When ...

mongoose memory leak attributed to jest

UPDATED 2020-09-14 I've encountered an issue with a test case I wrote. While the testcase passes, it raises a complaint about improper teardown and an open connection. Can anyone help identify the problem: Approach to Solving the Issue - Memory Leak ...

Is there a way to define an object's keys as static types while allowing the values to be dynamically determined?

I am attempting to construct an object where the keys are derived from a string union type and the values are functions. The challenge lies in wanting the function typings to be determined dynamically from each function's implementation instead of bei ...

Creating Meta tags for Dynamic Routes in a Nuxt 3 Build

I recently encountered an issue when trying to implement dynamic OpenGraph meta tags on a dynamically generated route in Nuxt 3 (and Vue 3). I attempted to set the meta tags dynamically using Javascript, as it was the only dynamic option supported by Nuxt ...

Struggling with intricate generic type mapping of records in Typescript

Whew...spent an entire day on this. My brain is fried... I am developing a type-safe mapper function that determines which props are needed based on the mapping type and can predict the output types based on the ReturnType. However, it seems like each re ...

Common problems encountered post Typescript compilation

I encountered the same problem. Below is my tsconfig settings: "compilerOptions": { "module": "commonjs", "moduleResolution": "node", "newLine": "LF", &q ...

Converting Typescript fat arrow syntax to regular Javascript syntax

I recently started learning typescript and I'm having trouble understanding the => arrow function. Could someone clarify the meaning of this JavaScript code snippet for me: this.dropDownFilter = values => values.filter(option => option.value ...

Create a function that takes in a function as an argument and generates a new function that is identical to the original function, except it has one

I want to establish a function called outerFn that takes another function (referred to as innerFn) as a parameter. The innerFn function should expect an object argument with a mandatory user parameter. The main purpose of outerFn is to return a new functio ...

Having trouble retrieving values from Promise.allSettled on Node.js 12 using TypeScript version 3.8.3

Currently, I am delving into NodeJs 12 and exploring the Promise.allSettled() function along with its application. The code snippet that I have crafted allows me to display the status in the console, but there seems to be a hitch when attempting to print t ...

The function for utilizing useState with a callback is throwing an error stating "Type does not have

Currently, I am implementing the use of useState with a callback function: interface Props { label: string; key: string; } const [state, setState] = useState<Props[]>([]); setState((prev: Props[]) => [...pr ...

a dedicated TypeScript interface for a particular JSON schema

I am pondering, how can I generate a TypeScript interface for JSON data like this: "Cities": { "NY": ["New York", [8000, 134]], "LA": ["Los Angeles", [4000, 97]], } I'm uncertain about how to handle these nested arrays and u ...

Incorporating XMLHttpRequest in my React Typescript App to trigger a Mailchimp API call, allowing users to easily sign up for the newsletter

My website needs to integrate Mailchimp's API in order for users to subscribe to a newsletter by entering their email into a field. I am looking to implement this without relying on any external libraries. To test out the functionality, I have set up ...

Tips for continuously running a loop function until retrieving a value from an API within a cypress project

Need help looping a function to retrieve the value from an API in my Cypress project. The goal is to call the API multiple times until we receive the desired value. let otpValue = ''; const loopFunc = () => { cy.request({ method: &ap ...

Typescript: Determining the return type of a function based on its input parameters

I've been working on a function that takes another function as a parameter. My goal is to return a generic interface based on the return type of the function passed in as a parameter. function doSomething <T>(values: Whatever[], getter: (whatev ...

Selecting ion-tabs causes the margin-top of scroll-content to be destroyed

Check out the Stackblitz Demo I'm encountering a major issue with the Navigation of Tabs. On my main page (without Tabs), there are simple buttons that pass different navparams to pre-select a specific tab. If you take a look at the demo and click t ...

Is it possible to switch the hamburger menu button to an X icon upon clicking in Vue 3 with the help of PrimeVue?

When the Menubar component is used, the hamburger menu automatically appears when resizing the browser window. However, I want to change the icon from pi-bars to pi-times when that button is clicked. Is there a way to achieve this? I am uncertain of how t ...

Transferring information via Segments in Ionic 3

I'm facing a challenge where I need to transfer a single ion-card from the "drafts" segment to the "sent" segment by simply clicking a button. However, I am unsure of how to achieve this task seamlessly. Check out this image for reference. ...

What steps do I need to take for webpack to locate angular modules?

I'm currently in the process of setting up a basic application using Angular 1 alongside Typescript 2 and Webpack. Everything runs smoothly until I attempt to incorporate an external module, such as angular-ui-router. An error consistently arises ind ...

Error: The file named '/accounts.ts' cannot be recognized as a module within a Node.js API

After researching this issue, I have found some answers but none of them seem to solve my problem. Below is the code in my model file: // accounts.ts const mongoose = require('mongoose'); var autoincrement = require('simple-mongoose-autoi ...