What is preventing the TypeScript compiler from accepting this code that appears to be valid?

I am encountering difficulties comprehending why the TypeScript type-checker is denying code that appears to be valid.

export type Fn<I, O> = (value: I) => O
type FInput<F extends Fn<any, any>> = F extends Fn<infer I, any> ? I : never
type FOutput<F extends Fn<any, any>> = F extends Fn<any, infer O> ? O : never

type FnMap = {
  numToInt: Fn<number, number>
  numToStr: Fn<number, string>
  strToNum: Fn<string, number>
  strToStr: Fn<string, string>
}

const fnMap: FnMap = {
  numToInt: (x) => Number(x.toFixed(0)),
  numToStr: (x) => x.toString(10),
  strToNum: (x) => parseInt(x),
  strToStr: (x) => x.toUpperCase(),
} as const

function doWork<T extends keyof FnMap>(key: T, input: FInput<FnMap[T]>): FOutput<FnMap[T]> {
  const fn = fnMap[key]

  return fn(input)
  // Type 'string | number' is not assignable to type 'FOutput<FnMap[T]>'.
  //   Type 'string' is not assignable to type 'FOutput<FnMap[T]>'.
  // Argument of type 'string | number' is not assignable to parameter of type 'never'.
  //   Type 'string' is not assignable to type 'never'.
}

In the provided example, TypeScript correctly auto-completes the doWork function (you can experiment with it on the playground).

Is there a particular reason for this failure? Is there another way to express this?

Answer №1

The reason your code isn't working is due to the compiler's struggle in evaluating conditional types that depend on generic type parameters. When invoking a function of type FnMap[T] within doWork(), and passing an argument of type FInput<FnMap[T]>, the compiler fails to connect this with FOutput<FnMap[T]>. This confusion arises because FInput and FOutput are conditional types, while T is generic, leading to ambiguity for the compiler. Consequently, it widens T to its constraint but faces uncertainties about input suitability and output correctness.

To resolve this issue, refactoring is recommended to explicitly represent things in terms of indexed accesses on mapped types as discussed here. By defining functions like (arg: XXX) => YYY for specific XXX and YYY, the compiler can deduce direct relationships between inputs and outputs more effectively. While these changes may seem minor from a human perspective compared to the original version, they significantly improve compiler understanding.

To demonstrate with your sample code, begin by defining the mapping object and deriving types:

// Mapping Object
const _fnMap = {
  numToInt: (x: number) => Number(x.toFixed(0)),
  numToStr: (x: number) => x.toString(10),
  strToNum: (x: string) => parseInt(x),
  strToStr: (x: string) => x.toUpperCase(),
}
type _FnMap = typeof _fnMap;

// Mapped Types
type FnMapArgs = { [K in keyof _FnMap]: Parameters<_FnMap[K]>[0] };
type FnMapRets = { [K in keyof _FnMap]: ReturnType<_FnMap[K]> }
type FnMap = { [K in keyof _FnMap]: (input: FnMapArgs[K]) => FnMapRets[K] };

// Assigning Equivalent Types
const fnMap: FnMap = _fnMap;

With non-generic specific types defined, the compiler can now process them effectively. By specifically defining indexes through mapped types, we update doWork() as follows:

function doWork<K extends keyof _FnMap>(type: K, input: FnMapArgs[K]): FnMapRets[K] {
  return fnMap[type](input); // No errors arise
}

Within this function, fnMap[type] is interpreted by the compiler as FnMap[K], enabling seamless interaction based on assigned mappings. As a result, the function generates an output of type FnMapRets[K].

For further exploration, refer to the https://www.typescriptlang.org/play?#code/HYQwtgpgzgDiDGEAEApeIA2AvJBvAsAFBFJKiSwLIAKATgPYBGGEYeJpSEAHjPbQBckAgJ4...ongoURL.

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

Is there a way to define a return type conditionally depending on an input parameter in typing?

I need help ensuring that a function in TypeScript returns a specific type based on a parameter. How can I make TypeScript understand my intention in this scenario? type X = 'x' type Y = 'y' const customFunc = <Type extends X | Y> ...

Include module A in module B, even though module A has already included module B's Angular

Currently, I am facing an issue with circular dependencies between two modules - User and Product. The User Module has already imported the Product Module to utilize the ProductListComponent. Now, I need to import/use the UserListComponent from the User Mo ...

Having trouble with the .d.ts module for images?

I'm relatively new to Typescript and the only thing that's giving me trouble is the tsconfig.json. My issue revolves around importing images (in Reactjs) and them not being found: client/app/Reports/View.tsx:11:30 - error TS2307: Cannot find mod ...

Can a mandatory attribute be made non-essential within an intersection category?

Currently, I am customizing the Material UI date picker and exploring ways to make the required props optional since default values are already provided by the parent component. This is my current code: import React, { useState } from "react"; i ...

Updating the state of a nested array using React Hooks

After spending some time working with React Hooks, my main struggle has been dealing with arrays. Currently, I am developing a registration form for teams. Each team consists of a list of players (an array of strings). The goal is to allow users to add t ...

Tweaking the column name and data values within a mat table

Exploring Angular and currently using Angular CLI version 8.1.0. I have two APIs, as shown below: For bank details: [ { "strAccountHolderName": "Sarika Gurav", "strAccountNumber": "21563245632114531", "ban ...

Automatically injecting dependencies in Aurelia using Typescript

Recently, I started working with Typescript and Aurelia framework. Currently, I am facing an issue while trying to implement the @autoinject decorator in a VS2015 ASP.NET MVC 6 project. Below is the code snippet I am using: import {autoinject} from "aure ...

Setting IDPs to an "enabled" state programmatically with AWS CDK is a powerful feature that allows for seamless management of

I have successfully set up Facebook and Google IDPs in my User Pool, but they remain in a 'disabled' state after running CDK deploy. I have to manually go into the UI and click on enabled for them to work as expected. How can I programmatically e ...

Creating a JSON formatted post request with Angular 4 and Rails 5

Currently, I am in the process of developing an Angular application with a Rails backend. However, I seem to be encountering some difficulties when it comes to formatting the parameters hash to meet Rails' requirements. The data involves a many-to-man ...

Determining the parameter type of an overloaded callback: A comprehensive guide

I am currently working on creating a type-safe callback function in TypeScript with a Node.js style. My goal is to define the err parameter as an Error if it exists, or the data parameter as T if not. When I utilize the following code: export interface S ...

Determine the point where a cube intersects a plane using Three.js

Given a plane and a cube, I am interested in determining if they intersect. If they do intersect, I would also like to find out: What shape does their intersection form - a triangle, a parallelogram or a hexagon? In degenerate cases, it may just be a p ...

Use the Express application as an argument for the http.createServer function

Encountering an error when trying to use express.Application as an argument for http.createServer: error TS2345: Argument of type 'Application' is not assignable to parameter of type '(request: IncomingMessage, response: ServerResponse) =&g ...

Unwrapping nested objects in a JSON array with JavaScript: A step-by-step guide

After trying some code to flatten a JSON, I found that it flattened the entire object. However, my specific requirement is to only flatten the position property. Here is the JSON array I am working with: [{ amount:"1 teine med 110 mtr iletau" comment:"" ...

Can anyone clarify what the term 'this' is referring to in this particular snippet of code?

Upon analyzing the following code snippet: export abstract class CustomError extends Error { abstract statusCode: number; constructor(message: string) { super(message); Object.setPrototypeOf(this, CustomError.prototype); } abstract seri ...

Guide to leveraging tanstack table v8 for sorting data within a specific date range

Received data from API: const abc = [ { date: '2023-12-8', value: 'mop' },{ date: '2023-10-8', value: 'qrs' } ] How can we create a date range using two input fields when the dates are in string forma ...

Unable to locate TypeScript's before() and after() methods

Having trouble running TypeScript tests with Jest/SuperTest - when running npm test, I encounter the following errors: Which package am I missing or not importing? FAIL test/test.ts ● Test suite failed to run test/test.ts:8:3 - error TS2304: Ca ...

Removing the semicolon from the end of a string in JavaScript

const arr = [ { "id": "753311", "role": "System Of Record (SOR)", "license": "Target", "DC": "Client · L2 (Inactive), Account · L1", "managedGeography": "North America · L2, International · L2", "mana ...

Tips for modifying the JSON format within a Spring and Angular project

I am utilizing Spring for the backend and Angular for the frontend. Below is my REST code: @GetMapping(path = "/endpoint") public @ResponseBody Iterable<Relations> getGraphGivenEndpointId(@RequestParam(value = "id") int id) { return ...

Ways to display only a specific color in an image

Looking to work with an image that contains predefined color blocks? Take this example picture as reference: https://i.sstatic.net/QlwvY.png Is there a method to display only certain colored areas while keeping the rest transparent? Note that no edge pat ...

Angular 11 is causing confusion by incorrectly applying the Http interceptor to sibling modules

Here is the structure of my modules: https://i.sstatic.net/zO9dE.png The HTTP interceptor I provided in core.module.ts is affecting the HTTP client in the translation.module.ts. This is how my core module is set up: @NgModule({ declarations: [DefaultLa ...