Typescript Syntax for Inferring Types based on kind

I'm struggling to write proper TypeScript syntax for strict type inference in the following scenarios:

  1. Ensuring that the compiler correctly reports any missing switch/case options
  2. Confirming that the returned value matches the input kind by type
type KindA = {kind:'a'};
type KindB = {kind:'b'};
type KindC = {kind:'c'};
type AllKinds = KindA | KindB | KindC;

function create<T extends AllKinds>(kind:AllKinds['kind']):T {
  switch(kind) {
    case "a": return {kind:'a'};
    case "b": return {kind:'b'};
    case "c": return {kind:'c'};
  }
}

create("a");

Playground

I am curious if achieving this level of type safety is feasible with the latest version of TypeScript.

My previous attempt, using

case "a": return {kind:'b'} as T;
, did not provide the type checking I require for the returned value.

Answer ā„–1

It is not advisable to return T in this scenario.

Here's the explanation:

type KindA = { kind: 'a' };
type KindB = { kind: 'b' };
type KindC = { kind: 'c' };
type AllKinds = KindA | KindB | KindC;

function create<T extends AllKinds>(kind: AllKinds['kind']): T {
  switch (kind) {
    case "a": return { kind: 'a' };
    case "b": return { kind: 'b' };
    case "c": return { kind: 'c' };
  }
}

const result = create<{ kind: 'a', WAAAT: () => {} }>("a")
result.WAAAT() // compiler but causes an error in runtime

In most cases, generic arguments should be dependent on input values.

Consider this example:

type KindA = { kind: 'a' };
type KindB = { kind: 'b' };
type KindC = { kind: 'c' };
type AllKinds = KindA | KindB | KindC;


const builder = <Kind extends AllKinds['kind']>(kind: Kind) => ({ kind })

const result = builder("a"); // {kind: 'a' }

Playground

Refer to this answer, this answer and my article for further context

Am I correct in understanding that with current TypeScript, one cannot have both?

The issue lies not in the switch statement but rather in the explicit return type. The return type cannot depend on a generic value that is not bound with function arguments.

However, it is possible to achieve what you desire:

type KindA = { kind: 'a' };
type KindB = { kind: 'b' };
type KindC = { kind: 'c' };
type AllKinds = KindA | KindB | KindC;

function create<Kind extends AllKinds['kind']>(kind: Kind): Extract<AllKinds, { kind: Kind }>
function create(kind: AllKinds['kind']) {
    switch (kind) {
        case "a": return { kind: 'a' };
        case "b": return { kind: 'b' };
        case "c": return { kind: 'c' };
    }
}

const result1 = create("a") // KindA
const result2 = create("b") // KindB
const result3 = create("c") // KindC

Playground

You may have noticed that I utilized function overloading. This makes the TS compiler less strict, offering a bit of flexibility while maintaining readability and inferring return types.

From what I know, function overloads behave bivariantly, giving you the choice of which option works best for your situation.

Answer ā„–2

The issue at hand is quite clear.

Type '{ kind: "a"; }' cannot be assigned to type 'T'.
  '{ kind: "a"; }' can be assigned to the constraint of type 'T',
  but 'T' may be instantiated with a different subtype of constraint 'AllKinds'.

This complication arises even with a simpler type restriction in place.

function foo<T extends string>(): T {
  return 'foo';
}

As evidenced by the error message below.

Type 'string' cannot be assigned to type 'T'.
  'string' can be assigned to the constraint of type 'T',
  but 'T' could be instantiated with a different subtype of constraint 'string'.

The root cause lies in declaring a return value as type T, when T does not match the type string. While T extends string, this simply means that T is a subtype of string. For instance, the type 'bar' is a subtype of string, therefore we can assign 'bar' to T. Consequently, we anticipate the returned value to be 'bar' rather than 'foo'.

The solution involves abstaining from using generics altogether. If the objective is to return a string, it suffices to explicitly state the return type as string, without introducing the notion of returning a value of subtype T of string.

function foo(): string {
  return 'foo';
}

Likewise, if the intention is to return a value of type AllKinds, the definition should directly indicate returning a value of type AllKinds, avoiding implications of returning a value belonging to a subtype T of AllKinds.

type KindA = {kind:'a'};
type KindB = {kind:'b'};
type KindC = {kind:'c'};
type AllKinds = KindA | KindB | KindC;

function create(kind:AllKinds['kind']): AllKinds {
  switch(kind) {
    case "a": return {kind:'a'};
    case "b": return {kind:'b'};
    case "c": return {kind:'c'};
  }
}

create("a");

Edit: Achieving your desired outcome necessitates dependent types, which TypeScript does not support. However, you can enhance type safety through crafting a custom fold function.

type Kind = 'a' | 'b' | 'c';

type KindA = { kind: 'a' };
type KindB = { kind: 'b' };
type KindC = { kind: 'c' };

type AllKinds = KindA | KindB | KindC;

function foldKind<A, B, C>(a: A, b: B, c: C): (kind: Kind) => A | B | C {
  return function (kind: Kind): A | B | C {
    switch (kind) {
      case 'a': return a;
      case 'b': return b;
      case 'c': return c;
    }
  }
}

const create: (kind: Kind) => AllKinds = foldKind<KindA, KindB, KindC>(
  { kind: 'a' },
  { kind: 'b' },
  { kind: 'c' }
);

With this approach, only a KindA value corresponds to 'a', a KindB value matches 'b', and a KindC value aligns with 'c'. Feel free to explore the demo for further clarity.

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

Understanding how to use TypeScript modules with system exports in the browser using systemjs

Iā€™m facing an issue while using systemjs. I compiled this code with tsc and exported it: https://github.com/quantumjs/solar-popup/blob/master/package.json#L10 { "compilerOptions": { "module": "system", "target": "es5", "sourceMap": true, ...

React Project Encounters NPM Installation Failure

I recently started delving into the world of React and experimenting with different examples. Everything was running smoothly until I attempted to start the server [npm start] and encountered an error as shown below. Despite my best efforts, I can't p ...

Inside the function() in angular 2, the value of 'this' is not defined

I've integrated a UIkit confirmation modal into my app. However, I'm encountering an issue when trying to click the <button> for confirmation. The this inside the function is showing up as undefined. Here's the snippet of code in quest ...

Understanding the significance of an exclamation point preceding a period

Recently, I came across this code snippet: fixture.componentInstance.dataSource!.data = []; I am intrigued by the syntax dataSource!.data and would like to understand its significance. While familiar with using a question mark (?) before a dot (.) as in ...

Using Angular to handle routes with a specific domain prefix

Let's say I own the domain https://example.com and I'd like to create a subdomain specifically for my blog, like this: https://blog.example.com. How would you handle the routing for this scenario using Angular? ...

Exploring Service Injection and Factory Pattern in Angular 8

After going through various articles and official Angular guides, it appears that they were unable to assist me in solving my task. Here's what I wanted to achieve: Imagine having an Angular application with a product listing page. Additionally, this ...

Utilize tree-shaking functionality within your TypeScript project

In the process of developing a TypeScript telemetry library using OpenTelemetry, I am exploring ways to incorporate tree-shaking to allow consumers to selectively import only the necessary modules and minimize the overall bundle size. The project directory ...

Unable to implement a function from a controller class

I'm currently attempting to organize my Express.js code, but I've hit a snag when trying to utilize a class method from the controller. Here's an example of what my code looks like: product.service.ts export class ProductService { constr ...

Variable not accessible in a Typescript forEach loop

I am facing an issue with a foreach loop in my code. I have a new temp array created within the loop, followed by a nested foreach loop. However, when trying to access the temp array inside the nested loop, I encounter a "variable not available" error. le ...

Leveraging default values in generic implementations

Imagine a scenario where the following code is present: type QueryResult<ResultType = string, ErrorType = string> = { result: ResultType, } | { errors: ErrorType, } So, if I want to initialize my result, I can proceed like this: const myResult: ...

Using LitElement: What is the best way to call functions when the Template is defined as a const?

When the template is defined in a separate file, it's not possible to call a function in the component. However, if the template is defined directly as returning rendered HTML with this.func, it works. How can one call a function when the template is ...

Issue with the scoring algorithm using Angular and Spring Boot

Hello, I have created a scoring algorithm to calculate scores, but I encountered an error in "salaireNet". ERROR TypeError: Cannot read properties of null (reading 'salaireNet') at ScoringComponent.calculateScore (scoring.component.ts:33:55) ...

Issue with ESRI-Leaflet not displaying in an Angular Typescript environment due to a failure to recognize vector data

I am facing an issue where I cannot display the map or utilize the search functionality provided by esri-leafleft. Below is the code snippet from the typescript file: import { Component, OnInit } from '@angular/core'; import { Title, Meta } from ...

During my attempt to convert my Slice.js file to ts using the redux toolkit, I encountered some Type-errors

After creating a sample Redux toolkit with JavaScript files, I am now attempting to convert them to TypeScript. Some errors have been resolved, but I am facing issues with the following two errors: The error "Property 'name' does not exist on ty ...

Is your pure function component not receiving or responding to input props correctly?

Here is my code snippet: const actionCreators = { action: AppReducer.actionCreators.action } interface GlobalState { user: Model.User | null; } interface InputState { setStashBarWidth(width: number); stashWidth: number; } const Header = ...

retrieve the initial subarray from the array using rxjs

Looking to extract the first array from a subarray, my current setup is as follows: Map: map; Map() { Service }); } This is the interface structure: export interface map { } Encountering an error message: ERROR TypeError: ...

Creating a personalized Cache Module in Nest JS involves implementing a custom caching mechanism tailored to

I need help with implementing a custom CacheModule in NestJS. The guide only shows how to connect the cache directly to the main module of the AppModule application. What is the correct way to define and implement a custom cache module? My attempt at crea ...

Exploring Vue.js lifecycle events and when to begin loading store properties (Vue.observable)

Currently, I am utilizing Vue.observable() for state management and it is crucial for two store properties to be fully loaded before most views are rendered by vue-router. I have attempted implementing the loading logic in various lifecycle events such as ...

Having difficulty loading Angular2/ Tomcat resources, specifically the JS files

I am currently in the process of deploying my Angular2 web application on a Tomcat server. After running the ng build command, I have been generating a dist folder and uploading it to my Tomcat server. However, whenever I try to run my web app, I encounte ...

Is it possible to attach "traits" to a current array of objects using TypeScript?

I have a variety of object types that I need to manipulate within an array consisting of those object types. type AB = { a:number, b:number} type CD = { c:number, d:string} type DE = { d:number, e:boolean} let state: AB[] = [] function onStateChange(newSt ...