Advanced TypeScript Types - The issue of Unreachable errors within generic functions

I'm struggling to understand why the switch statement in my generic function isn't able to cast my types within its branches.

This is my code:

interface Id { id: number; }
enum Kind { square = "square", circle = "circle" }
interface Circle { kind: Kind.circle; radius: number; }
interface Square { kind: Kind.square; size: number; }
type Data = Circle | Square;
type ShapeModel<TData> = Id & TData;
class UnreachableError extends Error { public constructor(guard: never) { super(`Unsupported kind: ${JSON.stringify(guard)}`); } }

function myFunctionGeneric<TData extends Data>(data: TData): ShapeModel<TData> {
    switch (data.kind) {
        case Kind.circle:
            return { ...data, id: 1 };
        case Kind.square:
            return { ...data, id: 2 };
        default:
            throw new UnreachableError(data); // <-- UNEXPECTED
        // Argument of type 'TData' is not assignable to parameter of type 'never'.
        // Type 'Data' is not assignable to type 'never'.
        // Type 'Circle' is not assignable to type 'never'.
        // ts(2345)
    }
}

const myCircleData: Circle = { kind: Kind.circle, radius: 42 };
const mySquareData: Square = { kind: Kind.square, size: 42 };

// I expect this. Passing Circle and expecting to receive ShapeModel<Circle>
const myCircleModel: ShapeModel<Circle> = myFunctionGeneric(myCircleData);

// I expect this. Passing Square and wanting to receive ShapeModel<Square>
const mySquareModel: ShapeModel<Square> = myFunctionGeneric(mySquareData);

This logic works flawlessly without using a generic TData.

Can someone shed light on why TypeScript struggles to determine the type within the switch branch?

Answer №1

In order for a type to undergo narrowing, it must be a union type. However, in this case, the type is the generic parameter TData, which does not fall into that category. While TData may extend a union, TypeScript will not attempt to narrow it. This would pose a challenge as TData could potentially be a subtype of Circle, making it impossible to directly narrow down to Circle within switch cases without some conditional logic.

A straightforward solution would be to use a public signature with generics and a private signature with simpler unions that TypeScript can effectively narrow down.


function myFunctionGeneric<TData extends Data>(data: TData): ShapeModel<TData>
function myFunctionGeneric(data: Data): ShapeModel<Data> {
    switch (data.kind) {
        case Kind.circle:
            return { ...data, id: 1 };
        case Kind.square:
            return { ...data, id: 2 };
        default:
            throw new UnreachableError(data); 
    }
}

Playground Link

Answer №2

In order for Typescript to perform a discriminated union, the variable must be recognized as a union, correct? You can achieve this by enforcing it with the argument type TData & Data. This allows Typescript to statically determine that your variable can specifically be narrowed down to either TData & Circle or TData & Square, just as anticipated, while preserving generic behavior. (Playground Link)

function myFunctionGeneric<TData extends Data>(data: Data & TData): ShapeModel<TData>
                                                  //  ^ this fixes it.

Answer №3

For enhanced clarity, it is important to focus on the updated signature for UnreachableError. Additionally, an unsupported type has been included in the union type as a demonstration. The code now successfully compiles and will throw an error if a value of the same type as the illegal one added to the union type is passed to your function:

interface Id { id: number; }
enum Kind { triangle = "triangle", pentagon = "pentagon" , anyShape = 'anyShape'}
interface Triangle { kind: Kind.triangle; height: number; base: number; }
interface Pentagon { kind: Kind.pentagon; sideLength: number; }
interface AnyShape { kind: Kind.anyShape; description: string; }

type Shape = Triangle | Pentagon | AnyShape;
type GeometricModel<TShape> = Id & TShape;
class UnreachableError extends Error { public constructor(guard: any) { super(`Unsupported shape: ${JSON.stringify(guard)}`); } }

function calculateGeometricModel<TShape extends Shape>(shape: TShape): GeometricModel<TShape> {
    switch (shape.kind) {
        case Kind.triangle:
            return { ...shape, id: 1 };
        case Kind.pentagon:
            return { ...shape, id: 2 };
        default:
            throw new UnreachableError(shape); 
    }
}
const myTriangleData: Triangle = { kind: Kind.triangle, height: 10, base: 6 };
const myPentagonData: Pentagon = { kind: Kind.pentagon, sideLength: 8 };
const myAnyShapeData : AnyShape = {kind: Kind.anyShape , description: 'aDescription'}
// Obtain GeometricModel<Triangle> by passing Triangle
const myTriangleModel: GeometricModel<Triangle> = calculateGeometricModel(myTriangleData);

// Obtain GeometricModel<Pentagon> by passing Pentagon
const myPentagonModel: GeometricModel<Pentagon> = calculateGeometricModel(myPentagonData);

If you attempt this:

const myAnyShapeModel: GeometricModel<AnyShape> = calculateGeometricModel(myAnyShapeData)

You will encounter the anticipated error message:

Error: Unsupported shape: {"kind":"anyShape","description":"aDescription"}
    at calculateGeometricModel

Answer №4

In JavaScript, the never type signifies a value that can never occur. Your code editor, like vscode for example, will immediately alert you to this. To update the definition for UnreachableError, replace never with any:

class UnreachableError extends Error { public constructor(guard: any) { super(`Unsupported kind: ${JSON.stringify(guard)}`); } }

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

The error message "./components/Avatar.tsx Error: Module '@babel/preset-stage-0' not found" appeared on the screen

Even after dedicating a few hours to research, I'm still unable to resolve an ongoing issue with Babel and Webpack. ): The solution must be simple. Here is my .babelrc: { "presets": ["@babel/preset-env", "@babel/preset-reac ...

Take action once the Promise outside of the then block has been successfully completed

Presented below is the code snippet: function getPromise():Promise<any> { let p = new Promise<any>((resolve, reject) => { //some logical resolve(data); }); p.finally(()=>{ //I want do something when ou ...

The ng-model-options in Angular 2 is set to "updateOn: 'change blur'"

Currently working with angular 2, I am seeking a solution to modify the behavior of ngModel upon Enter key press. In angular 1.X, we utilized ng-model-options="{updateOn: 'change blur'}". How can this be achieved in angular 2? For reference, her ...

Retrieving object information in the constructor using Angular and Typescript

When attempting to access an object's data within a constructor, it results in an "undefined" object. Even though it functions properly in the ngOnInit() function, the data which is about to be reset is required every time the component is initiated. ...

Can someone provide guidance on utilizing the map function to iterate through intricate components in TypeScript?

I am facing a challenge while trying to iterate through a complex object containing 'inner objects'. When using the map function, I can only access one level below. How can I utilize map and TypeScript to loop through multiple levels below? Whene ...

Does the JavaScript Amazon Cognito Identity SDK offer support for the Authorization Code Grant flow?

Is there a way to configure and utilize the Amazon Cognito Identity SDK for JavaScript in order to implement the Authorization Code Grant flow instead of the Implicit Grant flow? It appears that the SDK only supports Implicit Grant, which means that a Clie ...

Using TypeScript to specify a limited set of required fields

Can a custom type constraint be created to ensure that a type includes certain non-optional keys from an object, but not all keys? For instance: class Bar { key1: number key2: Object key3: <another type> } const Y = { key1: 'foo' ...

Is Typescript compatible with the AWS Amplify Express API?

I've been struggling to set up my Amplify API in TypeScript and then transpile it to JavaScript. I know it sounds like a simple process, but I could really use some guidance on how to do this effectively. So far, I haven't progressed beyond the ...

Enable the generation of scss.d.ts files with Next.js

I'm currently working on a project that requires the generation of .d.ts files for the scss it produces. Instead of manually creating these files, I have integrated css-modules-typescript-loader with Storybook to automate this process. However, I am ...

Leveraging editor.action.insertSnippet from a different plugin

I am attempting to enhance the functionality of VS Code by adding buttons to the status bar that automatically insert code snippets. I am utilizing this Extension for this purpose. Additionally, I have configured keybindings in my keybindings.json file whi ...

Error in Typescript: Property 'timeLog' is not recognized on type 'Console'

ERROR in src/app/utils/indicator-drawer.utils.ts:119:25 - error TS2339: Property 'timeLog' does not exist on type 'Console'. 119 console.timeLog("drawing") I am currently working with Typescript and Angular. I have ...

The function `pickupStatus.emit` is not defined

Currently, I have a driver's provider that monitors changes in my firestore database and updates the status of a driver pickup request. Here is the function responsible for this process: getDriverPickupRequest(id) { this.DriverCollection.doc<Driv ...

Guide on creating a Typescript Conditional type structure for Array elements that rely on each other

In my function, I am working with an array of objects that contain an icon key. If one index in the array has a value assigned to the icon key, then another index should also have a value. If one index leaves the icon key undefined, then another index shou ...

The TypeScript algorithm for inferring types in conditional statements

I am interested in understanding how Typescript infers types in signatures using conditional types. Example 1: demonstrates correct inference of T as number: type Args<T> = { value: T } function foo<T>(args: Args<T>) { return args; ...

Mastering the art of leveraging generics in conjunction with ngrx actions

Currently, I am in the process of developing an Angular 7 application that utilizes the NGRX store. In our application, we have numerous entities, each with its own dedicated list view. I decided to explore using generics with the NGRX store to avoid writi ...

Troubleshooting issues with importing modules in Typescript

I've embarked on a new Node.js project using Typescript and encountered some issues. Initially, my server setup in server.ts looked like this: const express = require("express") const app = express() app.listen(3000, () => { console. ...

Angular Material input field with disabled state and a tooltip

<div> <mat-form-field *> <input matInput #filterBox (keyup)="searchBox(filterBox.value)" disabled /> <mat-label>Filter</mat-label> </mat-form-field> </div> <mat-button-toggle-gro ...

Utilizing the backtick operator for string interpolation allows for dynamic value insertion

Greetings! Currently, I am delving into the world of Angular 2 and have stumbled upon some interesting discoveries. To enhance my knowledge of TypeScript, I decided to utilize the ScratchJS extension on the Chrome browser. During this exploration, I experi ...

What is the best way to identify a particular subtype of HTMLElement based on its tag name and then save that element within another one?

I have a function that generates a node and returns it. Here is an example implementation: addElement: function ({ parentNode, elementName, tagName, }) { // Creates and appends the new node. parentNode[elementName] = document.createEl ...

Integrate Angular 2 into the current layout of Express framework

After generating an express structure with express-generator, I ended up with the standard setup: bin bld node_modules public routes views app.js package.json Now, I want to enhance the views and routes directories by organizing them as follows: v ...