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

Can you explain the process for accessing a parent function in Angular?

I have a form component that inserts data into a database upon submission, and I need to update the displayed data in another component once it changes in the database. I attempted using ViewChild to invoke the necessary functions, but encountered issues w ...

eslint warning: the use of '$localize' is flagged as an "Unsafe assignment of an `any` value"

When using $localize, eslint detects errors and returns two specific ones: Unsafe assignment of an 'any' value and Unsafe any typed template tag. It's quite strange that I seem to be the only one facing this issue while working on the proje ...

Having trouble obtaining search parameters in page.tsx with Next.js 13

Currently, I am in the process of developing a Next.js project with the Next 13 page router. I am facing an issue where I need to access the search parameters from the server component. export default async function Home({ params, searchParams, }: { ...

Tips for fixing type declaration in a generic interface

Here is a simple function that constructs a tree structure. interface CommonItem { id: string parent: string | null } interface CommonTreeItem { children: CommonTreeItem[] } export const generateTree = <Item extends CommonItem, TreeItem extends ...

When using TypeScript, it is important to ensure that the type of the Get and Set accessors for properties returning a

Why is it necessary for TypeScript to require Get/Set accessors to have the same type? For example, if we want a property that returns a promise. module App { export interface MyInterface { foo: ng.IPromise<IStuff>; } export int ...

Is it possible to integrate TypeScript 5.0 decorators into React components?

Every time I add decorators to my class, they always get called with the arguments specified for legacy decorators: a target, property key, and property descriptor. I am interested in using TypeScript 5.0 decorators. Is this feasible, and if so, how can I ...

How to leverage async/await within loops in Node.js for optimized performance and efficiency

Hey there, I'm working on my nodejs api where I need to fetch data inside a loop and then perform another loop to save data in a different table. Can anyone guide me on how to achieve this? Below is a snippet of what I have attempted so far without su ...

You can easily search and select multiple items from a list using checkboxes with Angular and TypeScript's mat elements

I am in need of a specific feature: An input box for text entry along with a multi-select option using checkboxes all in one place. Unfortunately, I have been unable to find any references or resources for implementing these options using the Angular Mat ...

Exploration of mapping in Angular using the HttpClient's post

After much consideration, I decided to update some outdated Angular Http code to use HttpClient. The app used to rely on Promise-based code, which has now been mostly removed. Here's a snippet of my old Promise function: public getUser(profileId: nu ...

Changing the ngModel value within ngFor loop

I am working on a project where I need to display a list of grades from an object called 'grades'. Additionally, I want to integrate a slider component for each grade, with the value of the slider corresponding to a predefined list. However, it s ...

Error: Unable to generate MD5 hash for the file located at 'C:....gradle-bintray-plugin-1.7.3.jar' in Ionic framework

When attempting to use the command ionic cordova run android, an error occurred that prevented the successful execution: The process failed due to inability to create an MD5 hash for a specific file in the specified directory. This issue arose despite suc ...

Utilizing Angular 2's ngModel feature for dynamic objects and properties

Within my TypeScript file, I am dynamically generating properties on the object named selectedValsObj in the following manner: private selectValsObj: any = {}; setSelectedValsObj(sectionsArr) { sectionsArr.forEach(section => { section.questions. ...

Creating a TypeScript function that can dynamically assign values to a range of cells within a column, such as AD1, AD2, AD3, and so on

Hello there I'm currently working on a function that will dynamically assign values to the column range of AE to "AD" + i. However, when I use the function provided below, it only writes AD5 into the first 5 columns instead of AD1, AD2, AD3, and so o ...

When I define a type in TypeScript, it displays "any" instead

Imagine a scenario where we have a basic abstract class that represents a piece in a board game such as chess or checkers. export abstract class Piece<Tags, Move, Position = Vector2> { public constructor(public position: Position, public tags = nul ...

Could it be that the TypeScript definitions for MongoDB are not functioning properly?

Hello everyone, I'm facing an issue with getting MongoDB to work in my Angular 4 project. In my code, I have the db object as a client of the MongoClient class: MongoClient.connect('mongodb://localhost:27017/test', (err, client) => { ...

Developing personalized middleware definition in TypeScript for Express

I have been struggling to define custom middleware for our application. I am using [email protected] and [email protected]. Most examples of typing middleware do not involve adding anything to the req or res arguments, but in our case, we need to modify ...

Utilizing client extension for Postgres with Prisma to activate RLS: A step-by-step guide

Recently, I attempted to implement client extension as advised on Github. My approach involved defining row level security policies in my migration.sql file: -- Enabling Row Level Security ALTER TABLE "User" ENABLE ROW LEVEL SECURITY; ALTER TABLE ...

Convert JSON data to an array using Observable

My current task involves parsing JSON Data from an API and organizing it into separate arrays. The data is structured as follows: [ {"MONTH":9,"YEAR":2015,"SUMAMT":0}, {"MONTH":10,"YEAR":2015,"SUMAMT":11446.5}, {"MONTH":11,"YEAR":2015,"SUMAMT":5392 ...

Having an issue with forkJoin where the code seems to get stuck and does not continue execution after

The following script is retrieving two values from the database. I am using forkJoin for this purpose, which is a new approach for me. The reason behind utilizing this method is that there is a specific function that requires both values to be fetched bef ...

Is it possible to enhance an interface by integrating the characteristics of a constant?

I am currently working on customizing a material-ui v4 Theme. Within our separate @our-project/ui package, we have the following: export declare const themeOptions: { palette: { // some colors missing from Palette } status: string; // other pro ...