Ways to selectively include a key in an object?

Let's explore different types with the following structure:

type Car = {
  make: string;
  model?: string;
}

type Bike = {
  make: number;
  model?: number;
}

We have an object defined as type Car that we want to convert into type Bike. This conversion involves overwriting certain keys and adding new keys conditionally based on the original object's properties:

const car: Car = {
  make: 'Toyota',
  model: 'Corolla'
}

const bike: Bike = {
  ...car,
  make: 2022,
  ...(car.model && {model: Number(car.model)})
}

// the expected result:
// const bike: Bike = {
//   make: 2022,
//   model: 22
// }

An error occurs in TypeScript with the message:

Type '{ model?: string | number | undefined; make: number; }' is not assignable to type 'Bike'.
  Types of property 'model' are incompatible.
    Type 'string | number | undefined' is not assignable to type 'number | undefined'.
      Type 'string' is not assignable to type 'number | undefined'.

This error raises questions about how model is inferred. Is there a workaround to address this issue?

Answer №1

This issue arises from a combination of two minor design constraints and one major limitation in TypeScript's design. To overcome this, it is recommended to refactor the code or utilize a type assertion.


The first constraint relates to microsoft/TypeScript#30506, where checking a property of an object does not narrow the type of the object itself unless it is part of a discriminated union type. This means that simply checking one property of an object may not lead to desired narrowing of the object's type. In cases like these, creating a custom type guard function can help in achieving the desired behavior.

function isBString(a: A): a is { a: string, b: string } {
  return !!a.b;
}
if (isBString(a)) {
  // Code block for narrowed type
}

The next issue is related to how the compiler handles objects whose properties are unions. The compiler does not consider an object with properties as equivalent to a union of objects, leading to unexpected errors during variable declarations.

type EquivalentA =
  { a: string, b: string } |
  { a: string, b?: undefined }
 
var a: EquivalentA; // Error due to type mismatch

This lack of equivalence recognition at the type level can result in errors even after making adjustments in the code.


Fundamental limitations within TypeScript's control flow analysis further aggravate the situation. The compiler's inability to retain narrowing information outside specific code scopes hinders accurate type inference and can lead to errors despite apparent logic for type matching.

To work around these limitations, alternative workflows or explicit type assertions can be utilized. While type assertions transfer the responsibility of ensuring type safety to the developer, they can be effective in scenarios where the compiler struggles to infer correct types.

A simplified approach could involve using type assertions when spreading objects into new literals, helping the compiler make more accurate assumptions about the resulting types:

const b = {
  ...a,
  a: 1,
  ...a.b && { b: Number(a.b) }
} as B

In situations where the compiler fails to verify type safety but manual inspection confirms correctness, leveraging type assertions becomes a viable option to resolve compilation issues.

Answer №2

It seems like you made some changes to your question and managed to find the solution on your own! :) The code I have is very similar to yours except for the final test.


type A = {
  a: string;
  b?: string;
};


type B = {
  a: number;
  b?: number;
};

/* With more generic object types:
type A = {
  [id: string]: string;
};


type B = {
  [id: string]: number;
};
*/

const a: A = {
  a: '1',
  b: '2'
}

const b: B = {
  ...a,   
  a: 1,   
  ...(a.b && { b: Number(a.b) })
}

console.assert(b.a === 1, 'b.a');
console.assert(b.b === 2, 'b.b');
console.log(b);

Executed as

tsc temp.ts && node temp.js
and the output was:

{ a: 1, b: 2 }

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

Can you explain the usage of the syntax in Angular marked with the @ sign, such as @NgModule, @Component, and @Injectable?

Angular utilizes specific syntax for declaring modules, components, and services, as shown in the example below: @Component({ ... }) export class AppComponent However, this syntax is not commonly seen in traditional JavaScript development. It begs the ...

Using Jest: A guide to utilizing a mocked class instance

When working on my frontend React application, I decided to use the auth0-js library for authentication purposes. This library provides the WebAuth class which I utilize in my code by creating an instance like so: import { WebAuth } from 'auth0-js&ap ...

Using Webpack and Typescript to Import Images

Currently, I am developing a React project with Webpack and Typescript. I need to include an image in one of my <img/> tags, but I am facing difficulties accessing the image files. webpack.config.js: ... module: { rules: [ ... ...

Angular 2 Google Chart: Defining column type using TypeScript

I am currently attempting to implement the Timeline chart functionality from the angular2-google-chart module for Google Charts. Unlike the other examples provided, this specific chart type requires a column type definition — a requirement not present in ...

TypeError: Unable to access the 'classify' property of an object that has not been defined (please save the ml5.js model first)

In my React app, I have set up ml5.js to train a model by clicking on one button and make predictions with another. However, I encounter an error when trying to test the model for the second time: TypeError: Cannot read property 'classify' of und ...

Creating a single submit handler for multiple forms in React

I am using a shared event handler for form submissions. handleSubmit = (e) => { e.preventDefault(); const errors = this.validate(); this.setState({ errors: errors || {} }); if (errors) return; this.doSubmit(); }; This event handle ...

Change parameter type in TypeScript

type T0 = { a: string b: string } type T1 = Omit<T0, 'b'> function func({ param }: { param: T0 | T1 }) { if (param.hasOwnProperty('b')) { /* reassign type */ } return param.b } Is it possible to change the type of param ...

Obtain the map field from Firestore and retrieve the map data

My firestore database has a field with Maps of data, and I am struggling to retrieve this information using a cloud function in Node.js. Despite trying numerous solutions from Stack Overflow and Google, the code snippet below is the only one that gives me ...

Please ensure that the function chain has appropriate parameter and return types by specifying the tuple type

Can a new type be created for the given tuple/array that has certain validation constraints? The following array is considered valid: const funcs = [(a: string) => 1, (a: number) => 'A', (a: string) => 2] However, this one is invalid ...

Encountering a Typescript issue while utilizing day classes from Mui pickers

Recently, I encountered an issue with my code that alters the selected day on a Mui datepicker. I came across a helpful solution in this discussion thread: MUI - Change specific day color in DatePicker. Although the solution worked perfectly before, afte ...

A guide on implementing directives in Angular 2

I am trying to load my navbar.html in my app.component.html by using directives and following the method below: Here is my navbar html: <p>Hi, I am a pen</p> This is my navbar.ts: import {Component, Directive, OnInit} from '@angular/c ...

Resolving type errors in Typescript React Tabs proves to be a challenging task

I am currently in the process of converting a Reactjs Component to a Typescript Component Below is the Tabs Component: <Tabs> <Tab label="one"></Tab> <Tab label="two"></Tab> </Tabs> import React, { ...

Mapping fetched JSON data to an existing TypeScript object: A step-by-step guide

Having trouble mapping fetched JSON data from the API to an existing object in TypeScript. Here is my code: https://i.sstatic.net/1UVg4.png This is my Hero Interface: export interface Hero { id: number; name: string; } When I console log: https:/ ...

Issue encountered with JavaScript function within TypeScript file connected to HTML code

I am currently working on a simple SharePoint web part and encountering an issue with using a function from another module file in my main file. Snippet from the JSFunctions.module.js file (where I define my function): function getApi(){ [my code]... }; ...

Is it compatible to use Typescript version 2.4.2 with Ionic version 3.8.0?

Is it compatible to use Typescript 2.4.2 with Ionic 3.8.0? $ ionic info cli packages: (C:***\AppData\Roaming\npm\node_modules) @ionic/cli-utils : 1.18.0 ionic (Ionic CLI) : 3.18.0 global packages: cordova (Cordova CLI) : not insta ...

Type of Data for Material UI's Selection Component

In my code, I am utilizing Material UI's Select component, which functions as a drop-down menu. Here is an example of how I am using it: const [criteria, setCriteria] = useState(''); ... let ShowUsers = () => { console.log('Wor ...

How to stop a loop of method calls that return a Promise<any> in TypeScript

My current issue involves a loop in which a method is called, and the method returns an object of type Promise<any>. I need to break the loop if the response from the method is correct. However, using the break statement does not stop the loop as exp ...

Using asynchronous data in Angular 2 animations

Currently, I have developed a component that fetches a dataset of skills from my database. Each skill in the dataset contains a title and a percentage value. My objective is to set the initial width value of each div to 0% and then dynamically adjust it t ...

Function to convert a property with a default value to an optional generic type

I created a function to validate FormData objects with Zod, using a generic type for flexibility across schemas. Here's the validate function: export function validate<T>( formData: FormData, schema: z.ZodSchema<T> ): { validatedD ...

What could be the reason for the esm loader not recognizing my import?

Running a small express server and encountering an issue in my bin/www.ts where I import my app.ts file like so: import app from '../app'; After building the project into JavaScript using: tsc --project ./ and running it with nodemon ./build/bin ...