Determine the type of input and output based on another argument

When working with a function that takes an object of either TypeA or TypeB, the first parameter is used to specify the type of the object and the returned type depends on this first parameter.

The issue arises in TypeScript where the type of the object is not inferred within a case (or if) statement, resulting in errors in the following code.

If you're like me and prefer to avoid explicit casting (such as adding as TypeA), are there any alternative solutions available?

type TypeA = {
  input: { foo: string }
  output: { foo: number }
}

type TypeB = {
  input: { bar: string }
  output: { bar: number }
}

type Types = {
  TypeA: TypeA
  TypeB: TypeB
}

type TypesName = keyof Types

/**
 * Handles TypeA or TypeB objects, returning the appropriate type based on the input.
 */
function transform<N extends TypesName>(typeName: N, typeValue: Types[N]['input']): Types[N]['output'] {
  switch (typeName) {
    case 'TypeA':
      return transformTypeA(typeValue) // Error message about assigning types may appear here
    case 'TypeB':
      return transformTypeB(typeValue) // Error message about assigning types may appear here
  }
  throw new Error('Unknown type')
}

function transformTypeA(typeA: TypeA['input']): TypeA['output'] {
  return { foo: parseInt(typeA.foo) }
}

function transformTypeB(typeB: TypeB['input']): TypeB['output'] {
  return { bar: parseInt(typeB.bar) }
}


const transformedValue = transform('TypeA', { foo: 'lol' })
console.log(transformedValue) // The transformedValue is now a type of { foo: number }

Answer №1

Although there is a potential solution, it may not be beneficial at this moment.

type Union<N extends keyof Types = keyof Types> = N extends N ? [typeName: N, typeValue: Types[N]['input']]: never;

function transform<N extends TypesName>(...p: Union<N>): Types[N]['output'] {
  switch (p[0]) {
    case 'TypeA':
      return transformTypeA(p[1])
    case 'TypeB':
      return transformTypeB(p[1])
  }
}

Playground Link

To ensure TypeScript recognizes the relationship between the two values, we need to utilize index access instead of defining separate parameters or destructuring them. This limitation might be addressed in the future, as mentioned here

The current pattern of deconstructing a discriminant property and payload property into distinct variables without establishing a connection is not supported due to limitations in the control flow analyzer. For instance:

type Data = { kind: 'str', payload: string } | { kind: 'num', payload: number };

function foo({ kind, payload }: Data) {
    if (kind === 'str') {
        payload.length;  // Error, payload not narrowed to string
    }
}

Possibly support for this pattern may be introduced later, but not within this PR.

Answer №2

In TypeScript version 4.6, developers can now utilize the feature requested in this wishlist item about supporting correlated union types #30581

type TypeA = {
  input: { foo: string }
  output: { foo: number }
}

type TypeB = {
  input: { bar: string }
  output: { bar: number }
}

type Types = {
  TypeA: TypeA
  TypeB: TypeB
}

type TypeMap = { 
  TypeA: TypeA, 
  TypeB: TypeB 
};

type TypeMapAsGeneric<K extends keyof TypeMap = keyof TypeMap> = { [P in K]: TypeMap[P] }[K];

function transformTypeA(typeA: TypeA['input']): TypeA['output'] {
  return { foo: parseInt(typeA.foo) }
}

function transformTypeB(typeB: TypeB['input']): TypeB['output'] {
  return { bar: parseInt(typeB.bar) }
}

const transformers: { [K in keyof TypeMap]: (data: TypeMapAsGeneric<K>['input']) => TypeMapAsGeneric<K>['output'] } = {
  TypeA: transformTypeA,
  TypeB: transformTypeB
};


const transform = <K extends keyof TypeMap>(typeName: K, inputValue: TypeMapAsGeneric<K>['input']): TypeMapAsGeneric<K>['output'] => {
  const transformer = transformers[typeName];
  return transformer(inputValue);
}


const transformedValue = transform('TypeA', { foo: '123' })
console.log(transformedValue)

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

Obtain a union type in TypeScript based on the values of a field within another union type

Is it feasible in Typescript to derive a union type from the values of a field within another union type? type MyUnionType = | { foo: 'a', bar: 1 } | { foo: 'b', bar: 2 } | { foo: 'c', bar: 3 } // Is there an automati ...

Using Next.js and Tailwind CSS to apply a consistent pseudo-random color class both on the server and client side

I am faced with a challenge on my website where I need to implement different background colors for various components throughout the site. The website is generated statically using Next.js and styled using Tailwind. Simply selecting a color using Math.ra ...

What is the best way to transform a string into emojis using TypeScript or JavaScript?

Looking to convert emoji from string in typescript to display emoji in html. Here is a snippet of the Typescript file: export class Example { emoji:any; function(){ this.emoji = ":joy:" } } In an HTML file, I would like it to dis ...

Can SystemJS, JetBrains IntelliJ, and modules be combined effectively?

Looking for some clarification on the functionality of module includes and systemJS within an Angular2 app structure. I have set up a basic Angular2 app with the following layout: -app |-lib (contains shims and node libraries) |-components |-app |-app. ...

Patiently waiting for the component variable to be assigned through subscription

I am facing an issue with two calls in my component. The second call depends on the result from the first call. In the first call, I set the value for my component variable "locked". The second call should only be executed when the result is true, meaning ...

Guidelines for utilizing a loader to handle a TypeScript-based npm module

I am currently facing a challenge with my React and JavaScript project as I attempt to integrate an npm module developed with TypeScript. The issue lies in configuring my project to compile the TypeScript code from this module, resulting in the error messa ...

Different varieties of TypeScript's keyof when working with objects

I am grappling with the concept of TypeScript's types when incorporating the keyof type operator on objects. Check out this example: type TypeA = { [k: number]: boolean }; type AKey = keyof TypeA; // ^? type AKey = number type TypeB = { [k: string] ...

Tips for verifying that parameters possess designated characteristics in TypeScript

How can I ensure that data2 and data3 work correctly, while data1 triggers an error if the data type is not specified as shown in the code below? I need to validate that when a user enters params like {name: 'aa', age: 20}, it should throw an er ...

Unable to store object data within an Angular component

This is a sample component class: export class AppComponent { categories = { country: [], author: [] } constructor(){} getOptions(options) { options.forEach(option => { const key = option.name; this.categories[k ...

Troubleshooting problems encountered in Nest.js due to modifications made within a service.ts file

I'm currently working on a Nest.js project and here is the content of the automobile.service.ts file: import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Car } from './enti ...

Outputting undefined values when processing an http post array

I seem to have encountered a major issue. Despite my efforts, I am seeing an undefined value when trying to display this JSON data. {"StatusCode":0,"StatusMessage":"OK","StatusDescription":{ "datas": [ {"sensor_serial":"SensorSerial1", "id":"11E807676E3F3 ...

"Elaborate" Typescript Conditional Generic Types

Scenario I am currently working on implementing strong typing for the onChange function of a UI library's Select component. To provide some context, the existing type definition for their onChange is as follows: onChange?: (...args: any[]) => v ...

It’s not possible for Typescript to reach an exported function in a different module

Having trouble referencing and using exported methods from another module. I keep getting an error that says 'There is no exported member in SecondModule'. module FirstModule{ export class someClass{ constructor(method: SecondModule ...

Issue with OpenAI's Rate Limit 429 Restriction

I've been experimenting with this repository in order to implement semantic search for YouTube videos using OpenAI + Pinecone. However, I keep encountering a 429 error at the following step - "Run the command npx tsx src/bin/process-yt-playlist.ts to ...

How do I implement branch code using TypeScript types in Svelte?

Looking for a solution similar to the one mentioned in Typescript: How to branch based on type, but tailored for Svelte. Despite implementing run-time type guards as suggested, Svelte continues to throw errors. I am dealing with an array called collectabl ...

Error encountered while compiling an Asp.Net Core project due to exceeding the maximum allowable path length in the

Encountering a critical error during the build process with Visual Studio 2016 update 3 Asp.Net Core. The build is interrupted with the following message: Severity Code Description Project File Line Suppression State Error MSB4018 The "FindC ...

Display a dynamic array within an Angular2 view

I have a dynamic array that I need to display in the view of a component whenever items are added or removed from it. The array is displayed using the ngOnInit() method in my App Component (ts): import { Component, OnInit } from '@angular/core' ...

Struggled to incorporate Typescript function overload's implementation

After reviewing the previous question: Typescript: exclude undefined/null properties from type In my TypeScript project, I attempted to create an overload for a function called request, which can optionally accept a payload. The function is defined by the ...

Proper positioning of try/catch block in scenarios involving delayed async/await operations

For the past six months, I have been utilizing async/await and have truly enjoyed the convenience it provides. Typically, I adhere to the traditional usage like so: try { await doSomethingAsync() } catch (e) {} Lately, I've delved into experimenti ...

Always deemed non-assignable but still recognized as a universal type?

I'm curious about why the never type is allowed as input in generic's extended types. For example: type Pluralize<A extends string> = `${A}s` type Working = Pluralize<'language'> // 'languages' -> Works as e ...