Parameter types retain their generality when constrained by other types

In my function, I have a generic parameter T and another parameter that is restricted based on the passed T.

For example:

const example = {
  x: 'hello',
  y: 'world',
} as const

function test<T extends object, K extends keyof T>(obj: T, key1: K, key2: K) {};

test(example, 'x', 'y') // Despite key1 narrowing K to 'x', K remains 'x'|'y'

My objective is to pass an object, constrain other parameters based on its type, and infer their values to further restrict other parameters:

  • The passed parameter infers T and restricts K
  • The passed second parameter infers K and restricts V
  • and so on

Answer №1

When dealing with multiple possible inference candidates for a generic type argument, a tradeoff arises. For example, consider a function like

declare function f<T>(x: T, y: T): void;

and then a call like

declare const a: A;
declare const b: B;
f(a, b); // <-- ?

What should be the expected behavior in this scenario? The compiler could either reject the call if A and B are not identical types, or it could allow any call by creating a union type A | B. Both approaches have their own use cases, and opinions on the desired behavior vary.

Currently, TypeScript employs heuristic rules to handle such situations, which generally work well in real-world scenarios but can sometimes be disappointing.


One of the key heuristics is that union types will not be generated unless the multiple candidates belong to the same primitive type. For instance, if instead of using both strings like "a" and "b", you used a string and a number, you would receive the expected error:

const foo = {
  a: 'hello',
  b: 'world',
  3: 'abc'
} as const

function bar<T extends object, K extends keyof T>(
  obj: T, key1: K, key2: K) { };

bar(foo, 'a', 3) // error
// ---------> ~  3 is not assignable to 'a'

But if both types are literal types that would widen to the same primitive type, like "a" and "b" being subtypes of string, TypeScript does generate union types to allow the call to succeed, as seen in your example.

Even though this may not be the desired behavior in your case, these heuristics generally work well in many other scenarios.


Is there a way to signal to the compiler that a different behavior is desired? For example, you might want to specify that key2 should only be compared against the type K</code inferred from <code>key1, without affecting the inference of K itself. This type of non-inferential type parameter usage is not directly supported in TypeScript, but there are discussed approaches to achieve it, such as defining a NoInfer<T> utility type:

function bar<T extends object, K extends keyof T>(
  obj: T, key1: K, key2: NoInfer<K>) { };

bar(foo, 'a', 'b') // error
// ---------> ~~~ 'b' not assignable to 'a'

Currently, there is no native implementation of NoInfer<T>, but workarounds like defining it yourself using

type NoInfer<T> = T & {}
can provide the desired behavior by giving it a lower inference priority compared to T.

Another approach would be to introduce an additional type parameter constrained to the first one, which can also prevent inference:

function bar<T extends object, K1 extends keyof T, K2 extends K1>(
  obj: T, key1: K1, key2: K2
) { };

bar(foo, 'a', 'b') // error

Either method should resolve the issue, at least in the given example.

Playground link to code

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

Utilizing CSS files to incorporate loading icons in a component by dynamically updating based on passed props

Is it possible to store icons in CSS files and dynamically load them based on props passed into a component? In the provided example found at this CodeSandbox Link, SVG icons are loaded from the library named '@progress/kendo-svg-icons'. Instea ...

When a webpage is moved, the globalProperties variable of "vue3 typescript" is initialized to null

main.ts const app = createApp(App) .use(router) .use(createPinia()) .use(vuetify) .use(vue3GoogleLogin, googleLogin) const globalProps = app.config.globalProperties; globalProps.isDebugMode = true; vue-shim declare ...

Unselected default option in Angular 4's select dropdown

My goal is to use Angular to retrieve a value from a variable and display it as the first option in a select element, while keeping the rest of the options below static. The issue I am facing is that even though Angular is fetching the data successfully, t ...

I'm having trouble grasping the issue: TypeError: Unable to access the 'subscribe' property of an undefined object

I've been working on a feature that involves fetching data from API calls. However, during testing, I encountered some errors even before setting up any actual test cases: TypeError: Cannot read property 'subscribe' of undefined at DataC ...

Understanding the functionality of imports within modules imported into Angular

I have been scouring through the documentation trying to understand the functionality of the import statement in JavaScript, specifically within the Angular framework. While I grasp the basic concept that it imports modules from other files containing expo ...

Struggling to map the response data received from an http Get request to a TypeScript object that follows a similar structure

Currently, I am invoking an http Get service method from a component to retrieve data and map it to a Person object. The goal is to display this information on the front end. Below is my component code: export class DisplayPersonComponent implements OnIni ...

The React Functional Component undergoes exponential re-renders when there is a change in the array

I'm encountering a problem with one of my functional components. Essentially, it maintains an array of messages in the state; when a new message is received from the server, the state should update by adding that new message to the array. The issue ar ...

Retrieve an Array Containing a Mix of Objects and Functions in Typescript

Let's address the issue at hand: I spent several months working with a custom React Hook using plain JavaScript, and here is the code: import { useState } from 'react'; const useForm = (initialValues) => { const [state, setState] = ...

Increasing response buffer size in Node.js fetch for version 2.x.x

Currently in the process of implementing an API request using nodejs-fetch and I've encountered an issue. The documentation states that the maximum buffer size for fetch is 16kB, but the response I need to retrieve is 53 kB. This causes the .fetch() f ...

Having difficulty transferring data between components using @Input syntax

I am having trouble passing the FailedProductId from Component1 to Component2 using @Input. Below is the code snippet: export class Component1 implements OnInit { public FailedProductId="produt"; constructor(private employeeService: ProductService) {} ...

Collaborative service utilization in Angular 2

My goal is to create a shared service for my app. import { Injectable } from '@angular/core'; @Injectable() export class SharedService { testService() { console.log('share!'); } } However, when I attempt to inject this shared ...

How can we define a function using a generic type in this scenario using Typescript?

Here's a challenge that I'm facing. I have this specific type definition: type FuncType<T> = (value: T) => T I want to create a function using this type that follows this structure: const myFunc: FuncType<T> = (value) => valu ...

loading dynamic content into an appended div in HTML using Angular

Here is the HTML code from my app.component.html file: <button mat-raised-button color="primary" mat-button class="nextButton" (click)="calculatePremium()"> Calculate </button> <div id="calcul ...

Debugging TypeScript on a Linux environment

Approximately one year ago, there was a discussion regarding this. I am curious to know the current situation in terms of coding and debugging TypeScript on Linux. The Atom TypeScript plugin appears promising, but I have not come across any information ab ...

What is the best way to share a configuration value retrieved from the back end across all components of an Angular 6 application?

In the Web API's Web.config file, I have defined configurations like MAX_FILE_SIZE and others. I want to retrieve these configurations from the backend and make them available to all Angular 6 components globally. Could someone suggest the most effect ...

Storing information upon refresh in Angular 8

When it comes to inter-component communication in my Angular project, I am utilizing BehaviourSubject from RXJS. Currently, I have a setup with 3 components: Inquiry Form Where users enter an ID number to check for summon-related information. This data ...

Issue detected in React Rollup: the specific module 'name' is not being exported from the node_modules directory

Currently in the process of creating a library or package from my component. The tech stack includes React, Typescript, and various other dependencies. Encountering an error while using Rollup to build the package: [!] Error: 'DisplayHint' is ...

Employing an unchanging Map format for observation

I'm currently working on implementing a synchronization mechanism using observable and Map structures from Immutable.js. However, I'm encountering an issue where the Map is unable to function as an observable or perhaps I might be approaching it ...

Retrieving information from a data file by implementing a GraphQL Apollo Server within a NextJS application route

Currently working with Next.js 14 (app route), React, and the GraphQL Apollo framework. I have a JSON file containing data saved locally that I'd like to display using the server API. How can I make this happen? Below is the JSON structure I need to r ...

What is the best way to incorporate auto-completion into a browser-based editor using Monaco?

Recently, I embarked on a project to develop a browser-based editor using monaco and antlr for a unique programming language. Following an excellent guide, I found at , gave me a great start. While Monaco provides basic suggestions with ctrl + space, I am ...