When conditionals are used, Typescript struggles to accurately infer the correct type

When using the same type (Options<ST extends SwitchType) for useStrategy options parameter and for toPayload options, I expected Typescript to infer the correct type for toPayload options. However, I encountered an error message:

The argument of type 'FirstOptions | SecondOptions' is not assignable to the parameter of type 'FirstOptions'. Property 'b' is missing in type 'SecondOptions' but required in type 'FirstOptions'.(2345)

Is this a limitation of Typescript or am I overlooking something?

enum SwitchType {
    First = 'first',
    Second = 'second'
}
export type FirstOptions = {
  a: string
  b: number
}
type SecondOptions = {
    a: string
}

export type Options<ST extends SwitchType> = ST extends SwitchType.Second ? SecondOptions : ST extends SwitchType.First ? FirstOptions : never

type Strategy<ST> = ST extends SwitchType.Second ? SecondStrategy : ST extends SwitchType.First ? FirstStrategy : never

type ToPayloadFunction<ST extends SwitchType> = (
  options: Options<ST>
) => any

type FirstStrategy = {
  toPayload: ToPayloadFunction<SwitchType.First>
}
type SecondStrategy = {
  toPayload: ToPayloadFunction<SwitchType.Second>
}

const getStrategy = <ST extends SwitchType>(type: ST): Strategy<ST> => {

    const firstStrategy: FirstStrategy = {
        toPayload: (options) => {
            console.log(options)
        }
    }

    const secondStrategy: SecondStrategy = {
        toPayload: (options) => {
            console.log(options)
        }
    }

if(type === SwitchType.Second) return secondStrategy as any
    if(type === SwitchType.First) return firstStrategy as any

    throw new Error('error')
}

const useStrategy = <ST extends SwitchType>(type: ST, options: Options<ST>): void => {
    const { toPayload } = getStrategy(type)
// Argument of type 'FirstOptions | SecondOptions' is not assignable to parameter of type //'FirstOptions'.
// Property 'b' is missing in type 'SecondOptions' but required in type 'FirstOptions'
    toPayload(options)
}

Answer №1

Let's explore this scenario:

enum SwitchType {
  First = 'first',
  Second = 'second'
}

export type FirstOptions = {
  a: string
  b: number
}

type SecondOptions = {
  a: string
}

export type Strategy = {
  [SwitchType.Second]: SecondOptions,
  [SwitchType.First]: FirstOptions
}

type StrategyHandlers = {
  [S in keyof Strategy]: {
    toPayload: (options: Strategy[S]) => void
  }
}

const firstStrategy: StrategyHandlers[SwitchType.First] = {
  toPayload: (options) => {
    console.log(options)
  }
}

const secondStrategy: StrategyHandlers[SwitchType.Second] = {
  toPayload: (options) => {
    console.log(options)
  }
}

const STRATEGY: StrategyHandlers = {
  [SwitchType.First]: firstStrategy,
  [SwitchType.Second]: secondStrategy
}


// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
  k: infer I
) => void
  ? I
  : never;


function getStrategy<ST extends SwitchType>(type: ST): UnionToIntersection<Values<StrategyHandlers>>
function getStrategy<ST extends SwitchType>(type: ST) {
  return STRATEGY[type]
}

type Values<T> = T[keyof T]

type Params = Values<{
  [T in SwitchType]: [T, Strategy[T]]
}>

const useStrategy = ([type, options]: Params): void => {
  const { toPayload } = getStrategy(type)

  toPayload(options)
}

toPayload function now operates with a union of functions, which may differ from your expectations and lead to errors. Check out my article for more insights.

I have simplified the code by removing conditional types, opting for a straightforward strategy pattern implementation using a Map data structure instead.

The updated toPayload now handles First or Second argument types via overloaded functions.

Although not foolproof, this setup allows unexpected input like:

const useStrategy = ([type, options]: Params): void => {
  const { toPayload } = getStrategy(type)

  toPayload({ a: 's' })
}

Refer to this answer for further clarification.

A more secure approach would involve:

enum SwitchType {
  First = 'first',
  Second = 'second'
}

export type FirstOptions = {
  a: string
  b: number
}

type SecondOptions = {
  a: string
}

export type Strategy = {
  [SwitchType.Second]: SecondOptions,
  [SwitchType.First]: FirstOptions
}

type StrategyHandlers = {
  [S in keyof Strategy]: {
    toPayload: (options: Strategy[S]) => void
  }
}

const firstStrategy: StrategyHandlers[SwitchType.First] = {
  toPayload: (options) => {
    console.log(options)
  }
}

const secondStrategy: StrategyHandlers[SwitchType.Second] = {
  toPayload: (options) => {
    console.log(options)
  }
}

const STRATEGY: StrategyHandlers = {
  [SwitchType.First]: firstStrategy,
  [SwitchType.Second]: secondStrategy
}

function getStrategy<ST extends SwitchType>(type: ST) {
  return STRATEGY[type].toPayload
}

const first = getStrategy(SwitchType.First)
const second = getStrategy(SwitchType.Second)

first({ a: '', b: 2 })
second({ a: '' })

Explore the code in this Playground.

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

Form submission returns JSON data with an undefined value from the server

I've been following a tutorial here but ran into some issues due to using newer versions of Angular and Ionic. This is an excerpt from my code: createReview(review){ let headers = new HttpHeaders(); headers.append('Content-Type&apo ...

Iterate through each item in an object using Angular

I attempted to utilize a forEach loop, but it's indicating that it's undefined for some reason. Here is my code snippet: var array: MoneyDTO[] = prices array.forEach(function (money: MoneyDTO) { if (money.currency == 'QTW& ...

Troubleshooting Puppeteer compatibility issues when using TypeScript and esModuleInterop

When attempting to use puppeteer with TypeScript and setting esModuleInterop=true in tsconfig.json, an error occurs stating puppeteer.launch is not a function If I try to import puppeteer using import * as puppeteer from "puppeteer" My questi ...

Angular 7 Forms: Implementing Conditional "Required" Validation with Reactive Forms

Before I get into it, I won't be able to share the actual code I'm working on due to confidentiality reasons. However, I can provide a simplified version of the code... I am working with Angular 7 and Reactive Forms. In my form, I have a radio b ...

Validation of route parameters in Angular 4

Currently, I have a predefined route that includes a parameter called userID. { path: "edit/:userID", component: EditUserComponent, canActivate: [AuthGuard] }, Within the edit-user-component.ts file, the following logic is implemented: ...

How can I retrieve the height of a dynamically generated div in Angular and pass it to a sibling component?

My setup consists of a single parent component and 2 child components structured as follows: Parent-component.html <child-component-1 [id]="id"></child-component-1> <child-component-2></child-component-2> The child-compo ...

What is preventing TypeScript from resolving assignment in object destructuring?

Consider the code snippet below: interface Foo { a?: number b?: number } function foo(options?: Foo) { const { a, // <-- error here b = a } = (options ?? {}) return [a, b] } Why does this code result in the followi ...

Error: Export keyword unexpectedly found in TypeScript project

Having a problem while running Jest in my TypeScript project. The TypeScript file is located at rootDir/src/_services/index.ts and Jest is throwing an error: ({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,je ...

Setting up the properties of an object directly from an array - all in one line

Here is my array: const a = ['one', 'two'] as const; Here is the type of object I'm working with: type T = { { [key in typeof a[number]]: number; } The expected result should look like this: const r: T = { one: 0, two ...

How can I arrange selected options at the top in MUI autocomplete?

I am currently working with mui's useAutocomplete hook https://mui.com/material-ui/react-autocomplete/#useautocomplete Is there a way to programmatically sort options and place the selected option at the top using JavaScript sorting, without resorti ...

The DAT GUI controls are mysteriously absent from the scene

Within a modal, I have set up a threejs scene with three point lights. All functions are exported from a separate file called three.ts to the modal component. The issue I am facing is that when I try to initialize DAT.GUI controls, they end up rendering ...

Caution - Unable to execute a function on a type that does not have a callable signature

Below is the code I am working with: export interface IStartCreate1 { (desc?: string, opts?: IDescribeOpts, arr?: Array<string | IDescribeOpts | TCreateHook>, fn?: TCreateHook): void; tooLate?: boolean; } export interface IStartCreate2 { (opt ...

Jasmine: utilizing unit test to spy on the invocation of a nested function

When running unit tests for an Angular app, I need to spy on a function called func1 to check if it is being called. However, within func1 there is a call to func2 and I also want to spy on that to see if it is being called. How should I structure my unit ...

The production build is encountering an issue with a type error stating that the property 'companies' does not exist on the 'PrismaClient' type, while the local build is successful

Currently, I am working on a nextjs project hosted on Vercel, utilizing TypeScript and Prisma. Here are the versions I am using: "next": "13.0.3" "typescript": "4.9.3" "prisma": "^4.6.1" My local build is successful, but I am encountering a failure on Ve ...

SPFX web part encounters an issue due to lack of support for property or method "includes" in Object

I am facing an issue with my spfx web part version 1.8.2 that is functioning well in Chrome but not in IE11. Initially, the error "Object doesn't support property or method 'find'" was occurring, leading me to include the following packages: ...

Tips for displaying an associative object array as td elements within a tbody in Nuxt

I'm having trouble displaying the property of an associative object array in my code. I attempted to utilize a v-for loop and wanted to showcase the property information within the td elements of a tbody. I am aware that v-data-table components have a ...

Issue: Module './App' not found in webpackSolution: Check if the module path is

I've decided to switch my .js files to .tsx in order to start using TypeScript. To incorporate TypeScript, I used the following command: yarn add typescript @types/node @types/react @types/react-dom @types/jest and began converting first index.tsx fo ...

Overloading TypeScript functions with Observable<T | T[]>

Looking for some guidance from the experts: Is there a way to simplify the function overload in the example below by removing as Observable<string[]> and using T and T[] instead? Here's a basic example to illustrate: import { Observable } from ...

Fire the props.onChange() function when the TextField component is blurred

Currently, I am in the process of developing a NumberField component that has unique functionality. This component is designed to remove the default 0 value when clicked on (onFocus), allowing users to input a number into an empty field. Upon clicking out ...

Angular 2 Date Input failing to bind to date input value

Having an issue with setting up a form as the Date input in my HTML is not binding to the object's date value, even though I am using [(ngModel)] Here is the HTML code snippet: <input type='date' #myDate [(ngModel)]='demoUser.date& ...