Using the spread operator for type checking of generics is overly broad

While experimenting with interface inheritance and generics, I came across a peculiar behavior that might lead to runtime problems. This issue is observed in the latest release of TypeScript, version 5.0.3. Essentially, it seems that a function accepting a generic value that extends an underlying interface can mistakenly return incorrectly typed values.

To illustrate this problem, consider the following example:

interface MaybeHasId {
  id?: string,
}

interface HasId extends MaybeHasId {
  id: string,
}

const replaceId = <T extends MaybeHasId>(item: T, newId?: string): T => {
  return {...item, id: newId}
}

const moreSpecificObject: HasId = replaceId({id: "specific id"}, undefined);
console.log(moreSpecificObject.id.length);

Playground

Is there a more robust approach for strongly typing scenarios like this? Could this be an inherent flaw in TypeScript? One would expect either a compile-time error raised by this code or stricter enforcement during the creation of inheritance-based types using "extends".

Answer №1

It's a known limitation of TypeScript that there is no direct type operator like `{...T, ...U}` to handle object spread with overwritten properties. You can check out the feature request regarding this at microsoft/TypeScript#10727, along with related issues such as microsoft/TypeScript#50185 and microsoft/TypeScript#50559.

When spreading objects of specific types, TypeScript prevents overwritten properties in the result:

const specific = { ...{ a: 1, b: "two" }, b: 2 };
/* const specific: {
    b: number;
    a: number;
} */

However, when dealing with values of generic types, TypeScript approximates the result as an intersection type, as highlighted in microsoft/TypeScript#28234:

function generic<T extends { a: number, b: string }>(t: T) {
  return { ...t, b: 2 };
}
/* function generic<T extends { a: number; b: string;}>(
      t: T
   ): T & { b: number; } 
*/

As explained in microsoft/TypeScript#28234,

The use of intersections seems to be the best approach for cases involving objects with overlapping property names and different types, balancing accuracy and complexity effectively.


In your code, the expression { ...item, id: newId } is considered to have the intersection type

T & {id: string | undefined}</code, making it assignable to <code>T
:

const replaceId = <T extends MaybeHasId>(item: T, newId?: string): T => {
  const ret = { ...item, id: newId };
  // const ret: T & { id: string | undefined; }
  return ret;
}

To address this in your example code, you can use a type assertion to derive a more accurate type using the Omit utility type:

const replaceId = <T extends MaybeHasId>(item: T, newId?: string) => {
  const ret = { ...item, id: newId };
  // const ret: T & { id: string | undefined; }
  return ret as Omit<T, "id"> & { id: string | undefined }
}

This adjustment will lead to the expected error prompt:

const moreSpecificObject: HasId = replaceId({ id: "specific id" }, undefined);
// -> ~~~~~~~~~~~~~~~~~~
// Type 'undefined' is not assignable to type 'string'.

Link to play around with the code on the TypeScript Playground

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

React Date-Picker is unable to process a date input

Recently, I've been working on integrating a date picker into my application. I came across this helpful library that provides a date picker component: https://www.npmjs.com/package/react-date-picker So far, I have set up the component in the follow ...

Placeholder for a constructor property method

Currently, I am in the process of writing unit tests for a function that utilizes the twilio-node package to send SMS messages. The specific function I am focusing on testing, both for arguments passed and number of times called, is Twilio.prototype.messag ...

Encountering a timeout error when trying to test the video element with Jest

My function extracts meta data such as width and height from a video element in the following code snippet: export async function getVideoMetadata( videoBlobUrl: string, videoElement: HTMLVideoElement, ): Promise<{ width: number; height: number }> ...

SolidJS directives utilizing use:___ result in TypeScript errors when used in JSX

As I work on converting the forms example from JS to TS, I came across a typescript error related to directives in HTML: It appears that validate and formSubmit are being recognized as unused variables by typescript, resulting in the following jsx error: ...

Passing a value to a property with a dynamically passed name in TypeScript

I'm encountering an eslint/typescript error message: errorUnsafe member access .value on an any value @typescript-eslint/no-unsafe-member-access when attempting to assign a value to a dynamically named property: selectChange(name: string, value: stri ...

"Exploring the process of retrieving URL parameters within an activated link using Angular 7 and executing a REST API call from a service

My aim is to retrieve data by utilizing the id field through Get parameters. Below is the URL code in my HTML that redirects to a specific page without triggering the service to fetch the REST API. <a [routerLink]="['/usedCars/detail', list ...

Updating a behavior object array in Angular 5 by appending data to the end

After creating a service to share data across my entire application, I'm wondering if it's possible to append new data to an array within the userDataSource. Here is how the service looks: user.service userDataSource = BehaviorSubject<Array& ...

Steer clear of chaining multiple subscriptions in RXJS to improve code

I have some code that I am trying to optimize: someService.subscribeToChanges().subscribe(value => { const newValue = someArray.find(val => val.id === value.id) if (newValue) { if (value.status === 'someStatus') { ...

React/Ionic: Avoiding SVG rendering using <img/> elements

I seem to be encountering an issue when trying to load SVG's in my React/Ionic App. I am fetching weather data from OpenWeatherMap and using the weather?.weather[0].icon property to determine which icon to display. I am utilizing icons from the follow ...

Error in TypeScript: Typography type does not accept 'string' type as valid

As I attempt to implement the Typography component from material-ui using TypeScript, I encounter a perplexing error message TypeScript is throwing an error: Type 'string' is not assignable to type 'ComponentClass<HTMLAttributes<HTMLE ...

When working with Angular/Typescript, the error message "compilation of 'export const' is not possible

Embarking on creating my very own Angular library, I took the first step by adding a service without any issues. The challenge arose when I decided to move a constant to a file named tokens.ts for the service to reference. Following this change, the build ...

I am searching for a way to retrieve the event type of a svelte on:input event in TypeScript, but unfortunately, I am unable to locate it

In my Svelte input field, I have the following code: <input {type} {placeholder} on:input={(e) => emitChange(e)} class="pl-2 w-full h-full bg-sand border border-midnight dark:bg-midnight" /> This input triggers the fo ...

Having trouble retrieving the Ionic 2 slides instance - getting a result of undefined

As I attempt to utilize the slides and access the instance in order to use the slideto functionality programmatically, I find myself encountering the issue of receiving 'undefined' back despite following the documentation. Here is my TypeScript ...

Using Typescript: ForOf Iteration with Unknown Value Types

My journey began with a quick peek at this particular inquiry. However, the approach discussed there utilized custom typing. I am currently iterating over object entries using a for-of loop. Here's a snippet of the values I'm dealing with below. ...

The unsightly square surrounding my sprite in Three.js

I am attempting to create a beautiful "starry sky" effect using Three.js. However, I am encountering an issue where my transparent .png star sprites have a colored outline around them. Here is the sprite I am using: https://i.sstatic.net/2uylp.png This ...

Can you conduct testing on Jest tests?

I am in the process of developing a tool that will evaluate various exercises, one of which involves unit-testing. In order to assess the quality of tests created by students, I need to ensure that they are effective. For example, if a student provides the ...

How can Angular send datetime data to Nodejs in the most effective manner?

Working with the primeng calendar component within a template-driven form, I encountered an issue. When passing the date 16/05/2018 11:45 from Angular to Node, it gets converted to 2018-05-16T06:15:33.000Z. I discovered that I could convert it back to IST ...

Refreshing the Mat Dialog content when removing items in Angular Material

I have successfully implemented a mat dialog table with 3 columns - name, email, and delete icon. When the user clicks on the delete icon, it prompts a confirmation message to confirm the deletion. Upon confirming, the item is removed from the database. Ho ...

Angular: No routes found that match the URL segment

I encountered an issue with my routes module where I am receiving the error message Cannot match any routes. URL Segment: 'edit-fighter' when attempting to navigate using the <a> link. The only route that seems to work is the champions-list ...

Ways to extract information from an Object and save it into an array

In my Angular2 project, I am working on retrieving JSON data to get all the rooms and store them in an array. Below is the code for the RoomlistService that helps me fetch the correct JSON file: @Injectable() export class RoomlistService { constructor( ...