How can we implement type guarding for a generic class in TypeScript?

Implementing a generic class in TypeScript that can return different types based on its constructor parameter.

type Type = 'foo' | 'bar';

interface Res {
    'foo': {foo: number};
    'bar': {bar: string};
}

class MyClass<T extends Type> {
    constructor(readonly type: T) {}

    public run(): Res[T] {
        if (is(this, 'foo')) {
            return { foo: 123 };
        }
        return { bar: 'xyz' };
    }
}

function is<T extends Type>(instance: MyClass<Type>, type: T): instance is MyClass<T> {
    return instance.type === type;
}

In the playground example, there's an error:

Type '{ foo: number; }' is not assignable to type 'Res[T]'. Type '{ foo: number; }' is not assignable to type '{ foo: number; } & { bar: string; }'. Property 'bar' is missing in type '{ foo: number; }' but required in type '{ bar: string; }'.

Despite the correct type guard for generic T, why is it not applied to the return line?

Answer №1

In TypeScript, the combination of generics and narrowing doesn't blend well together. When you utilize is(this, 'foo'), the type guard function can narrow down the apparent type of this to this & MyClass<"foo">, but it doesn't impact the generic type parameter T at all. The expectation that T would be constrained from Type to just "foo" is not met. Consequently, {foo: 123} might not be proven assignable to Res[T], resulting in an error.

It may seem intuitive that this.type === "foo" should imply that T is "foo", but this assumption is incorrect in general. This is because T could potentially be part of the union type "foo" | "bar", and confirming that this.type === "foo" wouldn't exclude that possibility entirely. At best, it can be said that you know T must intersect with

"foo"</c/>, making <code>T & "foo"
not equal to never, although enforcing or expressing such a constraint is challenging.

There have been multiple requests for features aimed at re-constraining generic type parameters through control flow analysis. Two significant ones related to this example are microsoft/TypeScript#27808, which aims to restrict T to only one of "foo" and "bar" rather than the entire union; and microsoft/TypeScript#33014, which suggests that even if T was the complete union, returning {foo: 123} would be safe. While either of these changes would likely resolve the issue, they are not currently incorporated into the language, necessitating workarounds.


The simplest workaround involves using type assertions to bypass strict type checking by the compiler:

public run(): Res[T] {
    if (is(this, 'foo')) {
        return { foo: 123 } as Res[T]; // fine
    }
    return { bar: 'xyz' } as Res[T]; // fine
}

This provides a quick solution with minimal modifications to your code. However, keep in mind that by employing type assertions, you assume the responsibility for accurate type declarations, as evidenced by the compiler accepting (!is(this, 'foo')).


If ensuring type safety guarantees from the compiler takes precedence over simplicity, refactoring away from type guarding and towards generic indexing can achieve the desired outcome. Instead of relying on control flow analysis, you can implement an indexed access type like so:

public run(): Res[T] {
    return {
        foo: { foo: 123 },
        bar: { bar: 'xyz' }
    }[this.type]; // valid
} 

While the logic might appear unconventional, this approach resembles the behavior of inspecting this.type and directing control flow based on the result. One notable distinction is that both potential return values {foo: 123} and {bar: 'xyz'} are computed during each execution of run(), albeit discarding one eventually. For simple object literals without side effects, the overhead is negligible. To replicate the previous behavior precisely, consider utilizing getters to execute only the appropriate code block corresponding to the actual this.type value:

public run(): Res[T] {
    return {
        get foo() { return { foo: 123 } },
        get bar() { return { bar: 'xyz' } }
    }[this.type]; // valid
}

In this scenario, when this.type is "foo", return {foo: 123} runs while return {bar: 'xyz'} does not.

Although adjustments can be made, the core concept revolves around implementing an indexed access type via an actual indexing operation instead of contingent statements like if/else.


To summarize, overcoming the limitations posed by the current absence of synergy between generics and type guards in TypeScript requires either relaxing type checks or leveraging generics independent of type guards.

Playground Link

Answer №2

It seems like restricting the return type of a function from within the function itself is not possible. One workaround is to resolve the issue with a typecast:

public run(): Res[T] {
    if (is(this, 'foo')) {
        return { foo: 123 } as Res[T];
    }
    return { bar: 'xyz' } as Res[T];
}

In the initial response, it was mentioned that Typescript might be having trouble narrowing the call to is(this, 'foo') due to the lack of T in the function definition for is:

function is<T extends Type>(instance: MyClass<Type>, type: Type): instance is MyClass<T>

A potential solution could involve modifying the function signature like so:

function is<T extends Type>(instance: MyClass<Type>, type: T): instance is MyClass<T>

This change would allow the compiler to narrow down based on the type of the second argument. Alternatively, specifying the generic argument during the function call could also address the issue, such as using is<'foo'>(this, 'foo').

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

Encountering a "No overload matches this call" error while using React, Typescript, and d3js

Encountered an error in Typescript while using the scaleLinear() function from d3js. Seeking assistance in resolving this issue. The code is in React and utilizes typescript. Below is the source code: import React, { useRef, useState, useEffect } from &apo ...

Encountered an error with create-react-app and MaterialUI: Invalid hook call issue

I am encountering an issue while trying to set up Create-react-app with Material UI. The error message I receive pertains to Hooks. Could there be something else that I am missing? This is the specific error message being displayed: Error: Invalid hook ...

Error encountered due to a circular reference in the dependency library

Whenever I attempt to run my application, I encounter the following error: > npm start Starting the development server... ts-loader: Using <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="42363b32273121302b323602716c776c71"& ...

Curious about the missing dependencies in React Hook useEffect?

I'm encountering the following issue: Line 25:7: React Hook useEffect has missing dependencies: 'getSingleProductData', 'isProductOnSale', and 'productData'. Either include them or remove the dependency array react-hoo ...

Leverage the useRef hook with React Draggable functionality

Having recently delved into coding, I find myself navigating the world of Typescript and React for the first time. My current challenge involves creating a draggable modal in React that adjusts its boundaries upon window resize to ensure it always stays wi ...

Challenges with sorting and pagination in Angular 6's material-table

I am facing a challenge in my Angular6 material-data-table application where I need to display and manipulate a complex JSON structure received from a REST endpoint. While the data is successfully displayed, I am struggling to implement pagination and sort ...

What are the steps to installing Typescript on my computer?

npm ERROR! encountered code EACCES during installation npm ERROR! while trying to create a directory npm ERROR! at path /usr/local/lib/node_modules/typescript npm ERROR! with error number -13 npm ERROR! Error: EACCES: permission denied, mkdir '/usr/lo ...

The bespoke node package does not have an available export titled

No matter what I do, nothing seems to be effective. I have successfully developed and launched the following module: Index.ts : import ContentIOService from "./IOServices/ContentIOService"; export = { ContentIOService: ContentIOService, } ...

Converting a string to HTML in Angular 2 with proper formatting

I'm facing a challenge that I have no clue how to tackle. My goal is to create an object similar to this: { text: "hello {param1}", param1: { text:"world", class: "bla" } } The tricky part is that I want to ...

Creating a canvas that adjusts proportionally to different screen sizes

Currently, I am developing a pong game using Angular for the frontend, and the game is displayed inside an HTML canvas. Check out the HTML code below: <div style="height: 70%; width: 70%;" align="center"> <canvas id=&q ...

What is the process for creating an Angular library using "npm pack" within a Java/Spring Boot application?

In my angular project, we have 5 custom libraries tailored to our needs. Using the com.github.eirslett maven plugin in our spring boot application, we build these libraries through the pom.xml file and then copy them to the dist/ folder. However, we also ...

What impact does setting 'pathmatch: full' in Angular have on the application?

When the 'pathmatch' is set to 'full' and I try to delete it, the app no longer loads or runs properly. import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { H ...

Learn how to automatically access keys in local storage using Angular

I need to implement a way to store data in local storage so that it persists even when the page is refreshed. In my HTML file, there is a button that triggers a function in my TypeScript file. This function takes a parameter 'game', which is an i ...

Discover the process of implementing nested service calls in Angular 2 by utilizing observables

Here are my component file and service file. I am trying to achieve that after the verification() service method is successfully called, I want to trigger another service method signup() within its success callback inside subscribe. However, I am encounter ...

Is there a method to add columns to an Angular material table dynamically?

I'm encountering an issue with creating dynamic tables using Angular Material tables. Since the table is reliant on an interface, I have a set number of columns. What I'm aiming for is to generate a table dynamically based on the server's re ...

Utilizing Mongoose Schema Enums Alongside TypeScript Enums

In our Typescript-based NodeJs project utilizing Mongoose, we are seeking the right approach to define an enum field on a Mongoose schema that aligns with a Typescript enum. To illustrate, consider the following enum: enum StatusType { Approved = 1, ...

"Is it possible to differentiate between a variable that is BehaviorSubject and one that is not

I am dealing with a variable that can be of type Date or BehaviorSubject<Date | null>. My concern is figuring out how to determine whether the variable is a BehaviorSubject or not. Can you help me with this? ...

OnDrop event in React is failing to trigger

In my current React + TypeScript project, I am encountering an issue with the onDrop event not working properly. Both onDragEnter and onDragOver functions are functioning as expected. Below is a snippet of the code that I am using: import * as React from ...

A Guide to Retrieving Parameters and Request Body using Express and Typescript

When I use the PUT method, I encounter this issue: const createFaceList = (req: Request<{faceListId : string}>, res: Response, next: NextFunction) => { console.log(req.body.name); console.log("faceListID = " + req.params.faceListId); a ...

The 'locale' parameter is inherently assigned the type of 'any' in this context

I have been using i18n to translate a Vue3 project with TypeScript, and I am stuck on getting the change locale button to work. Whenever I try, it returns an error regarding the question title. Does anyone have any insights on how to resolve this issue? ...