TypeScript conditional return type: effective for single condition but not for multiple conditions

In my code, I have implemented a factory function that generates shapes based on a discriminated union of shape arguments. Here is an example:

interface CircleArgs { type: "circle", radius: number };
interface SquareArgs { type: "square", length: number };

type ShapeArgs = CircleArgs | SquareArgs;

class Circle { constructor(_: CircleArgs) {}}
class Square { constructor(_: SquareArgs) {}}

type Shape = Circle | Square;

type ShapeOfArgs<Args extends ShapeArgs> = 
   Args extends CircleArgs? Circle :
   Square;

function createShape<Args extends ShapeArgs>(args: Args): ShapeOfArgs<Args> {
    switch (args.type) {
        case "circle": return new Circle(args);
        case "square": return new Square(args);
    }
}

This approach helps TypeScript infer the correct return type from the argument as demonstrated below:

const circle1 = createShape({type: "circle", radius: 1}); // inferred as Circle
const square1 = createShape({type: "square", length: 1}); // inferred as Square

Even when wrapping the call in another function, the return type is still propagated correctly:

class Container { insert(_: Shape): void {}; }

function createShapeIn<Args extends ShapeArgs>(args: Args, cont: Container) {
    const shape = createShape(args);
    cont.insert(shape);

    return shape;
}

const cont = new Container();
const circle2 = createShapeIn({type: "circle", radius: 1}, cont); // still inferred as Circle
const square2 = createShapeIn({type: "square", length: 1}, cont); // still inferred as Square

However...

Introducing one more type called Figure breaks the implementation.

Let's add a new type of shapes named Figure:

interface FigureArgs { type: "figure", amount: number };
class Figure { constructor(_: FigureArgs) {}}

The updated union types become:

type ShapeArgs = CircleArgs | SquareArgs | FigureArgs;
type Shape = Circle | Square | Figure;

As a result, the conditional type needs to be modified to accommodate the changes:

type ShapeOfArgs<Args extends ShapeArgs> = 
    Args extends CircleArgs? Circle :
    Args extends SquareArgs? Square :
    Figure;

Unfortunately, after making these adjustments, the transpilation fails with errors for each shape type assignment in the switch statement.

function createShape<Args extends ShapeArgs>(args: Args): ShapeOfArgs<Args> {
    switch (args.type) {
        case "circle": return new Circle(args); // error: Circle not assignable ShapeOfArgs<Args>
        case "square": return new Square(args); // error: Square not assignable ShapeOfArgs<Args>
        case "figure": return new Figure(args); // error: Figure not assignable ShapeOfArgs<Args>
    }
}

What am I overlooking in this setup?

PS: Though using the as operator may offer a workaround, it undermines the purpose of type checking.

Links to TS playground:

Answer №1

A straightforward approach would involve creating a "dictionary of types" rather than using unions and conditional types:

// Dictionary of types
type ShapeByType = {
    circle: Circle,
    square: Square,
    figure: Figure
}

function createShape<Args extends ShapeArgs>(args: Args): ShapeByType[Args["type"]] {
    switch (args.type) {
        case "circle": return new Circle(args);
        case "square": return new Square(args);
        case "figure": return new Figure(args);
    }
}

const circle1 = createShape({type: "circle", radius: 1});
//    ^? Circle

// ...

const circle2 = createShapeIn({type: "circle", radius: 1}, container);
//    ^? Circle

This method facilitates scalability to accommodate any number of Shapes.

Playground Link

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

Determining the quantity of variations within a union in Typescript

Is it possible to determine the number of types in a union type in Typescript, prior to runtime? Consider the following scenario: type unionOfThree = 'a' | 'b' | 'c'; const numberOfTypes = NumberOfTypes<unionOfThree>; c ...

Having trouble launching React application on local machine due to missing node modules

I am completely new to React. I recently forked a project on Github and am attempting to run it on my own machine. However, I've noticed that the folder structure is missing the node modules. Does this mean I need to install create-react-app globally ...

Tips for running batch files prior to debugging in VS Code

Currently, I am working on a project using Typescript, nodeJS, and VS Code. When it comes to debugging in VS Code, I have set up configurations in my launch.json file. { "type": "node", "request": "launch", "name": "La ...

Differentiating Typescript will discard attributes that do not adhere to an Interface

Currently, I am working with an API controller that requires a body parameter as shown below: insertUser(@Body() user: IUser) {} The problem I'm facing is that I can submit an object that includes additional properties not specified in the IUser int ...

Looking to transform a timestamp such as "2021-07-18T9:33:58.000Z" into a more readable format like 18th July for the date or 9:33 am for the time using Angular?

Is there a way to convert the Timestamp format "2021-07-18T9:33:58.000Z" to display as 18th July (for date) or 9:33 am (for time) in an Angular 11 application? Currently, my code looks like this: const myDate = new DatePipe('en-US').transform ...

Retrieve and showcase information from Firebase using Angular

I need to showcase some data stored in firebase on my HTML page with a specific database structure. I want to present the years as a clickable array and upon selecting a year, retrieve the corresponding records in my code. Although I managed to display a ...

Obtain the last used row in column A of an Excel sheet using Javascript Excel API by mimicking the VBA function `Range("A104857

Currently in the process of converting a few VBA macros to Office Script and stumbled upon an interesting trick: lastRow_in_t = Worksheets("in_t").Range("A1048576").End(xlUp).Row How would one begin translating this line of code into Typescript/Office Scr ...

Dynamic Object properties are not included in type inference for Object.fromEntries()

Hey there, I've been experimenting with dynamically generating styles using material UI's makeStyles(). However, I've run into an issue where the type isn't being correctly inferred when I use Object.fromEntries. import * as React from ...

No updates found (Angular)

When a button is clicked, a test method is triggered with i as the index of an element in an array. The test method then changes the value of the URL (located inside the sMediaData object) to null or '' and sends the entire sMediaData to the pare ...

The initial invocation of OidcSecurityService.getAccessToken() returns null as the token

Our internal application requires all users to be authenticated and authorized, including for the home page. To achieve this, we use an HttpInterceptor to add a bearer token to our API requests. Initially, when rendering the first set of data with the fir ...

Creating robust unit tests for Node.js applications with the help of redis-mock

I am facing an issue while trying to establish a connection with redis and save the data in redis using the redis-mock library in node-typescript, resulting in my test failing. Below is the code snippet for the redis connection: let client: RedisClientTyp ...

Limit the type to be used for a particular object key in TypeScript

My pet categories consist of 'dog' and 'cat' as defined in the Pet type: type Pet = 'dog' | 'cat' I also have distinct types for allowed names for dogs and cats: type DogName = 'Jack' | 'Fenton&apos ...

"Introducing a brand new column in ng2-smart-table that is generated from an Object

Can anyone provide guidance on how to create a new column from an Object datatype? I'm struggling with this task. Below are the settings and data for better clarity: private settings = { columns: { firstName: { title: &apo ...

Creating an object type that accommodates the properties of all union type objects, while the values are intersections, involves a unique approach

How can I create a unified object type from multiple object unions, containing all properties but with intersecting values? For example, I want to transform the type { foo: 1 } | { foo: 2; bar: 3 } | { foo: 7; bar: 8 } into the type {foo: 1 | 2 | 7; bar: ...

Find the combined key names in an object where the values can be accessed by index

I am currently working on creating a function called indexByProp, which will only allow the selection of props to index by if they are strings, numbers, or symbols. This particular issue is related to https://github.com/microsoft/TypeScript/issues/33521. ...

What is the best way to retrieve entire (selected) objects from a multiselect feature in Angular?

I'm facing an issue with extracting entire objects from a multiselect dropdown that I have included in my angular template. Although I am able to successfully retrieve IDs, I am struggling to fetch the complete object. Instead, in the console, it dis ...

The function $$.generatePoint is not recognized in the Billboard.js library

Having some trouble with integrating billboard.js into my Vue project as an alternative to using d3.js. Struggling to get it working in both my repository and a vanilla Vue project. Anyone familiar with the process of getting billboard.js running smoothly ...

Tips for integrating Tailwind CSS into Create React App using React

I recently started using tailwindcss with my react app. I tried to follow the guide from tailwindcss but encountered various issues and bugs along the way. If anyone has advice on how to successfully start a project using tailwind and react, I would apprec ...

What is the best way to integrate AWS-Amplify Auth into componentized functions?

Issue: I am encountering an error when attempting to utilize Auth from AWS-Amplify in separate functions within a componentized structure, specifically in a helper.ts file. Usage: employerID: Auth.user.attributes["custom:id"], Error Message: Pr ...

Using TypeScript with React: Initializing State in the Constructor

Within my TypeScript React App, I have a long form that needs to dynamically hide/show or enable/disable elements based on the value of the status. export interface IState { Status: string; DisableBasicForm: boolean; DisableFeedbackCtrl: boolean; ...