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

Exploring SVG Graphics, Images, and Icons with Nativescript-vue

Can images and icons in SVG format be used, and if so, how can they be implemented? I am currently utilizing NativeScript-Vue 6.0 with TypeScript. ...

An Unexpected Typescript Error Occurred While Creating an RxCollection With RxDB

I'm new to RxDB and I've come across a strange Typescript error in my Electron project. Here are the relevant parts of my code: import RxDB, { RxCollection, RxDatabase } from "rxdb"; RxDB.plugin(require("pouchdb-adapter-idb") ...

When an import is included, a Typescript self-executing function will fail to run

Looking at this Typescript code: (()=> { console.log('called boot'); // 'called boot' })(); The resulting JavaScript is: (function () { console.log('called boot'); })(); define("StockMarketService", ["require", "exp ...

Click on a link to open it in the current tab with customized headers

In my Angular project, I am attempting to open a link by clicking a button that redirects to the specified URL using the following code: window.open(MY_LINK, "_self"); However, in this scenario, I also need to include an access token in the header when t ...

Sorting data by percentages in AngularJS

I am currently facing an issue with sorting percentages in a table column. Despite using methods like parseFloat and other AngularJS (1.5.0) sorting techniques, the percentages are not being sorted as expected. [ {percentage: 8.82} {percentage: 0. ...

What could be causing my Page to not update when the Context changes?

In my Base Context, I store essential information like the current logged-in user. I have a User Page that should display this information but fails to re-render when the Context changes. Initially, the Context is empty (isLoaded = false). Once the init fu ...

The TypeScript compiler generates a blank JavaScript file within the WebStorm IDE

My introduction to TypeScript was an interesting experience. I decided to convert a simple JavaScript application, consisting of two files, into TypeScript. The first file, accounts.ts, contains the main code, while the second one, fiat.ts, is a support f ...

Creating types for React.ComponentType<P> in Material-UI using TypeScript

I am currently working with Typescript and incorporating Material-UI into my project. I am trying to define the component type for a variable as shown below: import MoreVert from '@material-ui/icons/MoreVert' import { SvgIconProps } from '@ ...

The compilation of the module has encountered an error with the PostCSS loader. There is a SyntaxError at line 2, character 14 indicating an unknown

I am developing an Angular 8 application. Currently, I am incorporating AlertifyJs into my project. In the styles.css file of Angular, I have imported these libraries: @import '../node_modules/alertifyjs/build/alertify.min.js'; @import '. ...

Exploring the functionality of Material components within a nested child component

I am facing an issue with my TestComponent, which uses a <mat-stepper> in its template. Due to the specific context of the stepper, I have to programmatically move to the next step instead of using the matStepperNext directive on a button. Here is a ...

What is the method to determine the length of a string with TypeScript?

Looking to derive the numerical length of a string: type Length = LengthOfString<"hello"> // ^? should equal 5 Feeling a bit lost on how to approach this. Any guidance on how to achieve this? (Currently diving into typescript's typ ...

Vue 3 - Compelled to utilize any data type with computedRef

Recently, I've been diving into Vue/Typescript and encountered a puzzling error. The issue revolves around a class named UploadableFile: export class UploadableFile { file: File; dimensions: Ref; price: ComputedRef<number>; ... constr ...

Is it possible to optimize the performance of my React and TypeScript project with the help of webpack?

I am working on a massive project that takes 6 to 8 minutes to load when I run npm start. Is there a way to speed up the loading process by first displaying the sign-in page and then loading everything else? ...

Launching a Node.js command-line interface to NPM, developed using TypeScript

I'm struggling with deploying my Node CLI tool to NPM. During development and testing, everything works fine. I can even use `npm link` on the repo without any issues. After successfully publishing and downloading the package, the application crashes ...

"Learn how to pass around shared state among reducers in React using hooks, all without the need for Redux

I've built a React hooks application in TypeScript that utilizes multiple reducers and the context API. My goal is to maintain a single error state across all reducers which can be managed through the errorReducer. The issue arises when I try to upd ...

Exploring ways to interact with an API using arrays through interfaces in Angular CLI

I am currently utilizing Angular 7 and I have a REST API that provides the following data: {"Plate":"MIN123","Certifications":[{"File":"KIO","Date":"12-02-2018","Number":1},{"File":"KIO","Date":"12-02-2018","Number":1},{"File":"preventive","StartDate":"06 ...

The Material UI button shifts to a different row

I need help adjusting the spacing between text and a button on my webpage. Currently, they are too close to each other with no space in between. How can I add some space without causing the button to move to the next line? const useStyles = makeStyles((the ...

Configuring environment variables during Jest execution

A variable is defined in my `main.ts` file like this: const mockMode = process.env.MOCK_MODE; When I create a test and set the variable to true, it doesn't reflect as `'true'` in the main file, but as `'false'` instead. describe ...

What is preventing React CLI from installing the template as TypeScript?

When I run npm init react-app new-app --template typescript, it only generates a Javascript template project instead of a Typescript one. How can I create a Typescript project using the CLI? Current Node JS version: 15.9.0 NPM version: 7.0.15 ...

Obtain the outcome of HTML5 FileReader by utilizing promises within an asynchronous function

I am encountering a challenge in my Angular 4 application where I am working with an image. I am trying to pass the base64 string to another variable, but due to the asynchronous nature of this process, the image.src ends up being empty. As a result, the ...