Typescript narrowing facing difficulty in isolating property type

I encountered an issue with my code:

interface Wide {
  prop: string | undefined
}

interface Narrow {
  prop: string
}

class Foo {
  prop: string
  constructor({ prop }: Narrow) {
    this.prop = prop
  }
}

const array = [{ prop: undefined }, { prop: 'foo' }]

array.forEach((obj: Wide) => {
  if (obj.prop && typeof obj.prop === 'string' && obj.prop !== undefined) {
    console.log(new Foo({ ...obj }))
    // Type 'undefined' is not assignable to type 'string'.
  }
})

In normal circumstances, I would expect Typescript to infer that when the if condition is met, it implies that the current obj being iterated over has a defined property prop with the type string. However, I am unable to make it work as expected.

Playground

Answer №1

To achieve the desired outcome, it is recommended to implement a Type guard (type predicate)

interface Wide {
  prop: string | undefined
}

interface Narrow {
  prop: string
}

class Foo {
  prop: string
  constructor({ prop }: Narrow) {
    this.prop = prop
  }
}

const array = [{ prop: undefined }, { prop: 'foo' }]

function isNarrow(obj: Wide | Narrow): obj is Narrow {
  return typeof obj.prop === 'string';
}

array.forEach((obj: Wide) => {
  if (isNarrow(obj)) {
    console.log(new Foo({ ...obj }));
  }
})

Interactive Demo

Answer №2

To resolve this issue, consider implementing a custom type predicate.

function isSpecific<T extends { prop: unknown }>(obj: T): obj is T & Narrow {
  return typeof obj.prop === 'string'
}

array.forEach((obj: Wide) => {
  if (obj.prop && isSpecific(obj)) {
    console.log(new Foo({ ...obj }))
  }
})

A couple of points to note:

  1. Copying the object with spread syntax may not be necessary. Passing the original object to the constructor could suffice.
  2. The condition involving obj.prop must include an additional check for empty string. Omitting obj.prop !== undefined is acceptable since a type of 'string' implies it cannot be undefined.

Alternatively, you can utilize type assertions as another solution.

array.forEach((obj: Wide) => {
  if (obj.prop && typeof obj.prop === 'string') {
    console.log(new Foo({ ...obj } as Narrow))
  }
})

Answer №3

That's because the obj is of type Wide, even though it contains a value of type string (and not undefined). In this scenario, you have more knowledge than the TypeScript compiler, so you can perform a type assertion like so:

if (obj.prop && typeof obj.prop === 'string' && obj.prop !== undefined) {
    let narrow: Narrow = obj as Narrow;
    console.log(new Foo(narrow));
}

PlayGroundLink

Answer №4

It seems like the compiler is able to correctly narrow the type of obj.prop:

if (obj.prop && typeof obj.prop === 'string' && obj.prop !== undefined) {
  console.log(new Foo({ prop: obj.prop }))
}

However, it lacks the intelligence to narrow down the type of the entire obj in this scenario. If you adjust Wide to be a union:

type Wide = Narrow | {prop: undefined}

then the second option will be filtered by the if statement.

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

invoke the next function a different privateFunction within rxjs

I'm trying to figure out how to pass the resetPassword data to the _confirmToUnlock method in Typescript/RxJS. Here is my subscribe method: public invokeUnlockModal() { let resetPassword = { userName: this.user?.userName}; //i need to send this ...

What is the process for transferring an existing collection or data in Firestore to a subcollection?

My firestore database currently has the following structure with documents: root | |---transactions/... I am looking to transfer all transactions to a new subcollection as shown below: root | |---users/user/transactions/... Any suggestions on how I can a ...

`How can default query parameters be configured for Routes in Angular 7?`

Within our Angular-7-Application, we rely on @ngrx and @ngrx/router-store to incorporate query params into the state. Many parts of the application consist of paginated lists. Each list is represented as a component with the Pagination-Component integrate ...

The NextJS image URL remains constant across various endpoints

Currently experiencing an issue with my Channel Tab where the icon and name of the channel are displayed. The problem is that every time I switch channels, the icon remains the same as the first channel I clicked on. PREVIEW This is the code I am current ...

Can discriminated unions solely be utilized with literal types?

When looking at the code snippet below, I encountered an issue with discriminating the union type using the typeof operator. function f(arg: { status: number; one: boolean } | { status: string; two: boolean }) { if (typeof arg.status === "number&q ...

Tips for concealing a react bootstrap modal while simultaneously executing the save function

When I click on the button with the onClick event set to {this.props.onHide}, the Modal is hidden successfully. However, I also need to execute the save function. When I try calling the save function along with the props, the save function is executed but ...

Testing a click event in Angular that triggers only the navigateByUrl function, utilizing Jasmin and Karma

Need help writing unit test cases for click events in Angular using Jasmine & Karma onClickCancel(): any { this.router.navigateByUrl('login'); } Seeking advice on how to write unit tests for click events that open Material dia ...

Creating personalized breakpoints in Material UI using TypeScript

When using the createMuiTheme() function, you have the ability to update breakpoint values like this. const theme = createMuiTheme({ breakpoints: { values: { xs: 0, sm: 600, md: 960, lg: 1280, xl: 1920, }, }, }) ...

Use the date-fns max function to identify the latest date in an array of date strings

The Dates string array is created using data from the backend object in the following manner: const dates: string[] = this.data.map((item:any) => item.date.jsdate); Here is the resulting array: dates = [ Thu Jan 12 2015 00:00:00 GMT+0530 (India T ...

What is the best way to link this to a function in AngularIO's Observable::subscribe method?

Many examples use the Observable.subscribe() function in AngularIO. However, I have only seen anonymous functions being used like this: bar().subscribe(data => this.data = data, ...); When I try to use a function from the same class like this: update ...

When implementing Swiper in TypeScript, an error is encountered: TypeError - Attempting to invoke the Swiper class constructor without using the 'new' keyword

I am facing an issue for which I couldn't find a solution, so I'm attempting to solve it myself for the first time. I am using TypeScript and Next.js, but unfortunately, Swiper does not offer direct support for Next.js and TS. Below is the TypeS ...

Utilizing a forwardRef component in TypeScript to handle child elements

Working with @types/react version 16.8.2 and TypeScript version 3.3.1. This forward refs example was taken directly from the React documentation with added type parameters: const FancyButton = React.forwardRef<HTMLButtonElement>((props, ref) => ...

How can I incorporate TypeScript paths into Storybook with Webpack version 5?

Storybook includes a pre-configured Webpack setup. Up until Webpack v4, it was possible to utilize tsconfig-paths-webpack-plugin and define custom tsconfig (or any other plugin) like so: const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plug ...

Is there a way to automatically close the previous accordion when scrolling to a new one on the page?

Currently, I am working with the material-ui accordion component and facing an issue where all accordions are initially opened. As I scroll down the page and reach a new accordion, I want the previous ones to automatically close. The problem arises when tr ...

Update each number in an array by appending a string within a table in an Angular component, rather than the

I have created a function that decides on a comment based on the result number added to an array and displays it in a table. However, calling this function within the template is causing a performance slowdown. Is there a way to achieve the same outcome w ...

What is the process for creating a coverage report for a TypeScript extension in vscode?

It appears that generating coverage reports with coveralls is not feasible for a VSCode extension built with TypeScript. Currently, I am incorporating test cases into our project https://github.com/PicGo/vs-picgo/pull/42. Despite exploring various methods ...

Type for the key in array.reduce that is not specific or unique

Given an array of objects, I use the reduce method to transform it into a new format where each key represents a date from the original array. For example, if the array contains objects with dates like {date: '22-02-06-00:55:66', name: 'one& ...

What could be causing my code to fail in retrieving lists from the SharePoint site?

I've been attempting to retrieve lists from a SharePoint site using the provided code, however, the list isn't appearing in the response. Could someone please offer assistance with this issue? I have tried various other solutions, but the problem ...

I am having trouble getting the bs-stepper to function properly within my Angular project

I am currently facing issues with the bs-stepper module in my Angular code. It is not functioning as expected and is throwing errors. Here is a snippet of my code: export class FileUploadProcessComponent implements OnInit { import Stepper from 'b ...

How can I define types for (const item of entries) in Typescript?

I attempted for (const ElementType as element of elements ) {} or for (const ElementType of elements as element) {} However, neither of these options is correct. ...