TypeScript failing to correctly deduce the interface from the property

Dealing with TypeScript, I constantly encounter the same "challenge" where I have a list of objects and each object has different properties based on its type. For instance:

const widgets = [
  {type: 'chart', chartType: 'line'},
  {type: 'table', columnWidth: 100}
];

I can define interfaces and types for the above scenario, which works well initially. However, eventually I face an issue where TypeScript fails to correctly identify the type of object I am working with. You can see an example in this playground example

Consequently, I often find myself having to resort to something like

(widget as WidgetChart).chartType

Browsing through some Stack Overflow posts, I came across a solution similar to this answer by @matthew-mullin, which mentions

Now you can use the Sublist type and it will correctly infer whether it is of type SublistPartners or SublistItem depending on the field values you provide.

Unfortunately, that doesn't seem to be the case in my situation.

Could it be that I am missing something or perhaps expecting too much from TypeScript?

Answer №1

Based on the code in your linked playground, TypeScript is correctly inferring your types. The issue arises from a misunderstanding of TypeScript due to this line (*per my understanding of your true intentions):

type Widget = WidgetChart | WidgetBase;

Specifically, considering WidgetBase's definition (from your linked playground),

enum WidgetType {
  CHART = 'chart',
  TABLE = 'table'
}
interface WidgetBase {
  type: WidgetType
  title: string
}

An object like

{ type: 'chart', title: 'some string' }
is a valid WidgetBase. Since
Widget = WidgetChart | WidgetBase
, this object also qualifies as a valid "Widget". It matches the case where type === 'chart' and doesn't include a reference to chartType. Hence, TypeScript rightly warns that chartType might not always exist on a Widget with a type of 'chart'.

To rectify this, you must provide TS only with the actual WidgetTypes:

enum WidgetType {
    CHART = 'chart',
    TABLE = 'table'
}
enum ChartType {
    LINE = 'line',
    BAR = 'bar'
}
interface WidgetBase {
    title: string
    // ... other common, *generic* properties ...
}
interface WidgetTable extends WidgetBase {
    type: WidgetType.TABLE
    columnWidth: number
    // ... other specific properties ...
}
interface WidgetChart extends WidgetBase {
    type: WidgetType.CHART,
    chartType: ChartType
    // ... other specific properties ...
}
type Widget = WidgetChart | WidgetTable; // <-- There are only two choices now: either a valid WidgetChart or a valid WidgetTable, nothing else! You can add more WidgetTypes here in a similar manner of course if you want

function renderWidget(widget: Widget){
    switch(widget.type) {
        case WidgetType.CHART:
            return console.log(widget.chartType); // this works now and doesn't complain :)
        case WidgetType.TABLE:
            return console.log(widget.columnWidth); // so does this! :)
        // you don't _need_ a default here in this case, since you've covered all the WidgetTypes
    }
}

Edit: An alternative:

If you have several widget types with matching properties (except for the type) and only a few needing extra properties, consider a different approach. For instance, widgets with identical properties but varying types could be styled differently, e.g., type = 'card-big' or type = 'card-small' etc.

In such cases, manually defining the extensions to WidgetBase for each type may not be feasible. Instead, employing Exclude and Omit from the TS Utility Types could simplify things. Here's how the code would look:

enum WidgetType {
    CHART = 'chart',
    TABLE = 'table',
    CARD_BIG = 'card-big',
    CARD_SMALL = 'card-small',
    // ... etc ...
}
interface WidgetBase {
    type: Exclude<WidgetType, WidgetType.CHART | WidgetType.TABLE>, // <-- A generic widget characterizes any widget *besides* chart or table
    title: string
    // ... other common, *generic* properties ...
}
interface WidgetTable extends Omit<WidgetBase, 'type'> { // <-- Extend base properties but omit clashing type
    type: WidgetType.TABLE
    columnWidth: number
    // ... other specific properties ...
}
interface WidgetChart extends Omit<WidgetBase, 'type'> {
    type: WidgetType.CHART,
    chartType: ChartType
    // ... other specific properties ...
}
type Widget = WidgetChart | WidgetTable | WidgetBase; // <-- Specify special cases + generic case without ambiguity 

function renderWidget(widget: Widget) {
    switch(widget.type) {
        case WidgetType.CHART:
            return console.log(widget.title, widget.chartType); // Specific widgets hold both generic and specific properties
        case WidgetType.TABLE:
            return console.log(widget.title, widget.columnWidth); // Same as above
        // ... Handle other generic widgets if needed, or ...
        default:
            return console.log(widget.title) // Other widgets will feature only generic properties
    }
}

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

What is the best way to preserve an enumeration value in TypeScript?

Is there a way to save enumeration values in TypeScript? For instance: createArticle(name: string, clr: ??enumeration??) { return axios.post(`${environment.apiUrl}/cards`, { card: `${name}`, color: ??clr?? }, ... } PS: Conte ...

Issues with NativeScript WebView displaying HTML file

Having trouble loading a local HTML file into a webview in my NativeScript (typescript) application. Despite using the correct path, it's not loading and instead shows an error. <WebView src="~/assets/content.html" /> An error message stati ...

What is the best way to automatically refresh an observable every 30 seconds?

@Component({ selector: 'app-geo', templateUrl: <img mat-card-image [src]="profileUrl | async "> export class GeoComponent implements OnInit { date; profileUrl: Observable<string>; constructor(private tempService ...

Is there a way to convert a typescript alias path to the Jest 'moduleNameMapper' when the alias is for a specific file?

I am currently working on setting up Jest in a TypeScript project. In our tsconfig.json file, we are using path aliases like so: "baseUrl": ".", "paths": { "@eddystone-shared/*": [ "../shared/*" ], "@eddystone-firebase-helpers/*": [ "src/helpers/fire ...

Generate a fresh array by filtering objects based on their unique IDs using Angular/Typescript

Hey there, I am receiving responses from 2 different API calls. Initially, I make a call to the first API and get the following response: The first response retrieved from the initial API call is as follows: dataName = [ { "id": "1", ...

Having trouble with react-responsive-carousel in Next.js version 13?

I have been following a tutorial to create an eBay clone. One of the steps involves creating a carousel. However, when I add it, the carousel does not transition to the next page. I have attempted to uninstall and reinstall packages, but the issue persists ...

Keeping track of important dates is ineffective using the calendar

I am facing an issue with developing a calendar that marks events on the correct dates. I am receiving the dates in the following format in PHP [ { "status": "OK", "statusCode": 200, "statusMensagem": & ...

Ways to verify if an item is an Express object?

Currently, I am utilizing typescript to verify whether an app returned by the Express() function is indeed an instance of Express. This is how I am attempting to accomplish this: import Express from "express" const app = Express() console.log( ...

Next.js does not recognize the _app file

After including the _app.tsx file in my project to enclose it within a next-auth SessionProvider, I noticed that my project is not recognizing the _app.tsx file. Even after adding a div with an orange background in the _app.tsx file, it does not reflect in ...

How can I showcase array elements using checkboxes in an Ionic framework?

Having a simple issue where I am fetching data from firebase into an array list and need to display it with checkboxes. Can you assist me in this? The 'tasks' array fetched from firebase is available, just looking to show it within checkboxes. Th ...

Can I create a unique Generic for every Mapped Type in Typescript?

I've got a function that accepts multiple reducers and applies them all to a data structure. For instance, it can normalize the data of two individuals person1 and person2 using this function: normalizeData([person1, person2], { byId: { init ...

I'm looking for a solution to reorganize my current state in order to display the image URL

My React component, which also utilizes TypeScript, is responsible for returning a photo to its parent component: import React, { useEffect, useState } from "react"; import axios from "axios"; export const Photo = () => { const [i ...

Unit testing in Typescript often involves the practice of mocking

One challenge with mocking in Typescript arises when dealing with complex objects, as is the case with any strongly-typed language. Sometimes additional elements need to be mocked just to ensure code compilation, such as using AutoFixture in C#. In contras ...

Tips for defining the type of any children property in Typescript

Currently, I am delving into Typescript in conjunction with React where I have a Team Page and a slider component. The slider component is functioning smoothly; however, my goal is to specify the type of children for it. The TeamPage import react, { Fragm ...

What is the best way to organize my NPM package with separate directories for types and functions?

I am currently working on developing a custom NPM package that will serve as a repository for sharing types and functions across my project. Let's name this project wordle. Given the emphasis on types, it is worth noting that I am using TypeScript for ...

Using TypeScript to pass the text field ID to a function for clearing the text field with a button

I am looking for a way to improve the functionality of my web page featuring several buttons that will clear different text boxes on the same line. Currently, I am using separate functions for each button, but my goal is to streamline this process by utili ...

Tips on utilizing a connected service in a custom Azure DevOps extension's index.ts file

I have created a unique extension for Azure DevOps that includes a specialized Connected Service and Build task. When setting up the task through the pipeline visual designer, I am able to utilize the Connected Service to choose a service and then populate ...

The design of Next.js takes the spotlight away from the actual content on the

Recently, I've been working on implementing the Bottom Navigation feature from material-ui into my Next.js application. Unfortunately, I encountered an issue where the navigation bar was overshadowing the content at the bottom of the page. Despite my ...

Efficient Typescript ambient modules using shorthand notation

Exploring the code snippet from the official module guide, we see: import x, {y} from "hot-new-module"; x(y); This syntax raises a question: why is 'x' not within curly brackets? What does this coding structure signify? ...

How to verify if an unknown-type variable in TypeScript contains a specific property

When using typescript with relay, the props passed down are of type unknown. Despite my efforts, I am unable to persuade the compiler that it can have some properties without encountering an error: <QueryRenderer environment={environment} query={te ...