Different methods to prompt TypeScript to deduce the type

Consider the following code snippet:

function Foo(num: number) {
  switch (num) {
    case 0: return { type: "Quz", str: 'string', } as const;
    case 1: return { type: "Bar", 1: 'value' } as const;
    default: throw new Error("Unknown discriminant: " + num);
  }
}

After analyzing this, TypeScript infers a discriminated union type like so:

function Foo(num: number): 
{ readonly type: "Quz"; readonly str: "string"; readonly 1?: undefined; } |
{ readonly type: "Bar"; readonly 1: "value"; readonly str?: undefined; }

However, I do not want TypeScript to infer this specific discriminated union type. Instead, I am looking for something different, like this:

{ type: "Quz"; str: "string"; } | { type: "Bar"; 1: "value"; }

I wish to avoid specifying a separate return type and also refrain from pre-evaluating any potential outputs in advance.

Is there a way to communicate to the TypeScript compiler the expected type of discriminated union that I have in mind?

Answer №1

When TypeScript infers types for values, it utilizes heuristic rules to ensure desirable behavior across various use cases. However, there are instances where these heuristics fall short of expectations.

One such rule involves unions of object literals being inferred with optional `undefined` properties from other members of the union. This transformation turns complex unions into discriminated unions, making them more manageable. Yet, in certain situations, this may lead to undesired type outcomes.


If TypeScript's type inference fails to align with your expectations, it is advisable to manually specify the expected type. By providing an annotation, you can guide the compiler and avoid unexpected inference results:

type DiscU = { type: "Quz"; str: "string"; } | { type: "Bar"; 1: "value"; };

function fooAnnotate(num: number): DiscU {
    switch (num) {
        case 0: return { type: "Quz", str: 'string', }; 
        case 1: return { type: "Bar", 1: 'value' };
        default: throw new Error("Unknown discriminant: " + num);
    }
}

In situations where manual type specification is not permitted, alternate approaches must be considered.


To circumvent pre-computation when dealing with object literals, a common workaround involves assigning the literal to an intermediate variable before building the union:

function foo(num: number) {
    const case0 = { type: "Quz", str: 'string' } as const;
    const case1 = { type: "Bar", 1: 'value' } as const;
    switch (num) {
        case 0: return case0;
        case 1: return case1;
        default: throw new Error("Unknown discriminant" + num);
    }
}

This method ensures the desired type output without engaging in precomputation.


An alternative approach to avoid precomputation is by using immediately-executed functions within the `switch` statement:

function foo(num: number) {
    switch (num) {
        case 0: return (() => ({ type: "Quz", str: 'string' } as const))();
        case 1: return (() => ({ type: "Bar", 1: 'value' } as const))();
        default: throw new Error("Unknown discriminant" + num);
    }
}

The immediate function effectively manages to prevent unwanted properties while adequately meeting requirements.


If the `readonly` properties remain a concern, modifying the immediately-executed function to a stand-alone function returning a non-`readonly` variant could address that issue:

function foo(num: number) {
    const mutable = <T extends object>(o: T): { -readonly [K in keyof T]: T[K] } => o;
    switch (num) {
        case 0: return mutable({ type: "Quz", str: 'string' } as const);
        case 1: return mutable({ type: "Bar", 1: 'value' } as const);
        default: throw new Error("Unknown discriminant" + num);
    }
}

By utilizing this modified function approach, the exact desired type can be achieved without extensive manual intervention or precomputation.

Explore the Playground Link for this code snippet!

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

React experimental is encountering an issue with missing property "" $fragmentRefs"" when relaying fragments

Details Recently, I decided to explore React experimental features with concurrent mode and relay. Even though I have never used relay before, I managed to make progress but ran into some issues. Initially, using the useLazyLoadQuery hook without any frag ...

Is it possible for me to create an interface that is derived from a specific type?

Is there a way to define an interface in TypeScript where the keys are based on a specific type? For example: type FruitTypes = "banana" | "apple" | "orange"; interface FruitInterface { [key: string]: any; // should use FruitTypes as keys instead of str ...

Are multiple click events needed for identical buttons?

In my component, there is an HTML structure like this: <div id="catalogo" class="container"> <div class="row"> <div *ngFor="let artista of artistas" class="col-sm" style="mar ...

Building Unique Password Validation with Angular 5

I'm attempting to implement custom password validation for a password field. The password must be a minimum of 8 characters and satisfy at least two of the following criteria but not necessarily all four: Contains numbers Contains lowercase letters ...

Encountering problem with React Typescript fetching data from Spring Data REST API: the error message "Property '_embedded' does not exist" is being displayed

I am currently working on a React application that utilizes Typescript to fetch data from a Spring Data REST API (JPA repositories). When I make a specific request like "GET http://localhost:8080/notifications/1" with an ID, my JSON response does not pose ...

How can I make sure that my function returns a mutated object that is an instance of the same class in

export const FilterUndefined = <T extends object>(obj: T): T => { return Object.entries(obj).reduce((acc, [key, value]) => { return value ? { ...acc, [key]: value } : acc; }, {}) as T; }; During a database migration process, I encounte ...

What methods can I utilize from Google Maps within Vuex's createStore()?

Currently, I am in the process of configuring my Vuex store to hold an array of Marker objects from the Google Maps Javascript API. Here is a snippet of how my createStore function appears: import { createStore } from "vuex"; export default ...

AngularJS 2: Updating variable in parent component using Router

My current app.component looks like the following: import { Component, Input } from '@angular/core'; import {AuthTokenService} from './auth-token.service'; @Component({ selector: 'app-root', templateUrl: './app ...

Error in Redux with TypeScript: "Argument of type 'AsyncThunkAction<any, number, {}>' is not compatible with parameter of type 'AnyAction'"

So I've been working on this dispatch function: const dispatch = useAppDispatch() const handleFetch = (e: React.MouseEvent<HTMLAnchorElement>) => { const target = e.target as Element dispatch(fetchCity(parseInt(ta ...

Quick + Vue Router - Lazy Loading Modules

For my personal project, I am using Vite alongside Vue 3 and have integrated vue-router@4 for managing routes. Since all of my modules share the same set of routes, I created a helper function: import { RouteRecordRaw } from 'vue-router' import p ...

Error message: Invariant Violation: Portal.render() being caused by semantic-ui-react Basic Modal

As part of enhancing an existing React component, I attempted to include a basic modal (link to documentation). Everything was working well without the modal, but once I added it in following the semantic-ui-react guidelines, I encountered a runtime error ...

`Why isn't GetServerSideProps being triggered for a nested page in Next.js when using Typescript?

I have been working on a page located at /article/[id] where I am trying to fetch an article based on the id using getServerSideProps. However, it seems that getServerSideProps is not being called at all as none of my console logs are appearing. Upon navi ...

Why does the property of {{hero.name}} function properly in a <h> tag but not in an <img> tag?

Within this template, the code below functions correctly: <h3>{{hero.name}}</h3> It also works for: <a routerLink="/details/{{hero.id}}">{{hero.name}}</a> However, there seems to be an issue with the following image path ...

Intro.js is not compatible with React and Remix.run

I am currently working on implementing onboarding modals for header links using intro.js within a React environment. Below is the code snippet: import { useState, type FC } from 'react' import type { Links } from '../types' import &apo ...

Exploring the potential of React with Typescript: Learn how to maximize

Having some difficulties working with Amplitude in a React and Typescript environment. Anyone else experiencing this? What is the proper way to import Amplitude and initialize it correctly? When attempting to use import amp from 'amplitude-js'; ...

What is the best way to save the output of an asynchronous function into a class attribute?

Currently, I am attempting to retrieve HTML content from a webpage by utilizing a class equipped with a single asynchronous method. This process involves Typescript 3.4.3 and request-promise 4.2.4. import * as rp from 'request-promise'; class H ...

Setting the data type for a React Stateless Functional Component (SFC) in TypeScript

By assigning a type of React.FC<PropsType> to a variable, it becomes recognized as a React Stateless Functional Component. Here's an example: //Interface declaration interface ButtonProps { color: string, text: string, disabled?: boolean ...

Is Highcharts-angular (Highcharts wrapper for Angular) compatible with Angular 4?

I have attempted to install various versions of highcharts-angular, ranging from 2.0.0 to 2.10.0. However, I consistently encounter the same error when running the application. The error message states: Metadata version mismatch for module C:/dev/Angular- ...

Having trouble locating the name WebGLObject in my TypeScript code

Every time I try to run ng serve command An error pops up on my screen saying: "WebGLObject cannot be found." ...

When the user clicks on a specific element, ensure that it is the main focus and generate an overlay

One of my challenges is implementing a custom element that captures user input upon clicking, focusing on it and overlaying other elements. I want the overlay to disappear if the user clicks outside the div. I attempted to achieve this using the iron-over ...