Is it possible to constrain generic indexed access parameters?

Consider the following scenario:

type Group =
  | {
      type: "string";
      payload: string;
    }
  | {
      type: "number";
      payload: number;
    };

A function can be created as shown below:

const groupFunction = <T extends Group>(group: T) => {
  console.log(group.type, group.payload);
};

If we attempt to call this function with incompatible types, an error will occur:

groupFunction({
  type: "number",
  payload: "hi", // Type 'string' is not assignable to type 'number'.ts(2345)
});

However, by utilizing indexed access in the function definition:

const anotherFunction = <T extends Group>(type: T["type"], data: T["payload"]) => {
  console.log(type, data);
};

We can now call this function without encountering any errors:

anotherFunction("number", "hi"); // no error

The question arises - can we use indexed access parameters to restrict the type of the second parameter based on the first parameter's type?

Answer №1

The issue at hand revolves around inference of generic type arguments. Within your implementation of call2(),

const call2 = <T extends Union>(type: T["type"], data: T["payload"]) => {
    console.log(type, data);
};

You might be anticipating the compiler to automatically determine the type T based on the fact that "number" is valid for T["type"]. However, this inference does not occur as expected. The compiler does not perceive T["type"] or T["payload"] as suitable points for inferring the type T. Consequently, the inference fails and falls back to its specified constraint which is Union, as evidenced by IntelliSense:

call2("number", "hi"); // no error
// const call2: <Union>(type: "string" | "number", data: string | number) => void

Given that Union["type"] is

"string" | "number"
, and Union["payload"] is string | number, both "number" and "hi" are respectively compliant with the types, resulting in no errors. Oops.

While it may seem reasonable to expect automatic inference, a feature like this was proposed in microsoft/TypeScript#20126 but never made its way into the language.

In scenarios where generic type arguments lack desired inference behavior, manual specification becomes an option. For instance:

call2<{ type: "number", payload: number }>("number", "hi"); // error
// ------------------------------------------------> ~~~~

However, striving for successful inference is preferable.


To facilitate type parameter inference, consider offering the compiler a value of the respective type. Rather than relying on T["type"] for inferring T, allow it to infer U from a value of type U. Relate your type parameter to the type argument within call2(), deriving the type of data accordingly. Utilize key remapping to establish this connection between keys and values:

type UnionMap = { [T in Union as T['type']]: T['payload'] }
/* type UnionMap = {
    string: string;
    number: number;
} */

The UnionMap aligns type keys with their corresponding payload values derived from Union via key remapping. With this structure, adjust the call signature for call2() like so:

const call2 = <K extends keyof UnionMap>(type: K, data: UnionMap[K]) => {
    console.log(type, data);
};

Now, the type argument conforms to the generic type K, enhancing the probability of successful inference, allowing the compiler to validate the passed argument against UnionMap[K]. Let's test the results:

call2("number", "hi"); // error
// -----------> ~~~~
// const call2: <"number">(type: "number", data: number) => void

This outcome indicates a satisfying result with the type argument K inferred as "number", thus expecting data to be of type

number</code, exposing the error with <code>"hi"
.


It's worth noting that this method isn't entirely foolproof. Just as you can convert a non-error scenario into an error by manually specifying a specific type argument, the converse is possible too. By defining the type argument as the full keyof UnionMap union yourself:

call2<keyof UnionMap>("number", "hi"); // okay
// const call2: <"string" | "number">(type: "string" | "number", data: string | number) => void

Since K represents keyof UnionMap, type defaults to

"string" | "number"
and data to string | number, accommodating the assignments of "number" and "hi". This aspect highlights a limitation addressed in an ongoing discussion mentioned in microsoft/TypeScript#27808, yet remains absent from the current language implementation.


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

Learn how to restrict input to only specific characters in an input box using Angular2 and validations

Is it possible to restrict user input in an input box to only specific characters such as '7' and '8'? I tried adding validations with attributes like type="number", min="7" and max="8", but even then other keys can be inserted before v ...

Determining the return type of a function by analyzing its argument(s)

I'm interested in defining a method within a class that will have its type based on the argument provided in the constructor. For example: class A { private model: any; constructor(model: any) { this.model = model; } getModel( ...

Creating autorest client based on various OpenAPI versions

I'm currently exploring options for creating a Typescript client from our .NET API. After researching various tools, I decided to go with Autorest, as it is Node-based and fits my skillset. While I am aware of Swashbuckle, my knowledge leans more towa ...

What is the recommended approach for sending a null value to a mandatory property in a Vue.js component?

Setup: Vue.js (3.2) with Composition API, TypeScript, and Visual Studio Code File type.ts: export class GeographicCoordinate { latitude: number; longitude: number; altitude?: number; constructor(latitude: number, longitude: number, altitude?: num ...

Challenges arise when attempting to break down an API into separate components rather than consolidating it into a

I've been struggling with this issue for a few days now. Problem Explanation: I am trying to use Axios to fetch data and store it in the state for each individual Pokémon. However, currently all the data is being rendered inside a single component w ...

There was a TypeScript error found at line 313, character 9 in the file @mui/material/styles/experimental_extendTheme.d

Encountering Typescript error while using Material UI component for date range picker Link - https://mui.com/x/react-date-pickers/date-range-picker/ Snippet of the code import * as React from 'react'; import { Dayjs } from 'dayjs'; im ...

The issue of a mocked MobX store in Jest not resetting between tests is causing problems

I have a straightforward login component and a MobX store that holds user information. I am testing the integration using Jest. The application is built with Create React App, so my tests are based on that. This is what my Login component looks like: cons ...

Setting style based on the condition of the router URL

I am currently facing an issue with a global script in Angular 10 that is supposed to evaluate the current path and apply a style to the navigation bar conditionally. However, it seems to fail at times when using router links. I am wondering if there is a ...

Insert information into a 3-tiered nested Angular FormArray within interconnected dropdown fields

After trying to retrieve data from an API call to populate a select form field, I encountered difficulties setting the value correctly using a FormArray. This led me to creating a FormArray with 3 nested levels in Angular, taking reference from this examp ...

Angular 6: A class with no 'default' modifier must explicitly specify a name

I am encountering this error in my ts.file, as I delve into the world of Angular/Ionic. Can anyone help identify the possible reasons for this? I have attempted multiple solutions to address it, but unfortunately, none have proven successful. import { Co ...

Bringing in information from a TypeScript document

I'm having trouble importing an object from one TypeScript file to another. Here is the code I am working with: import mongoose from "mongoose"; import Note from './models/notes'; import User from './models/users'; import ...

Using TypeScript's conditional types for assigning types in React

I'm tasked with creating a component that can belong to two different types. Let's call them Type A = { a: SomeCustomType } Type B = { b: SomeOtherDifferentType } Based on my understanding, I can define the type of this component as function C ...

Ways to import a library in JavaScript/TypeScript on a web browser?

I'm currently working on a project that involves a TypeScript file and an HTML page. Right now, I am loading the necessary libraries for the TypeScript file in the HTML Page using script tags like <script src="https://unpkg.com/<a href="/cd ...

Generate a pre-signed URL for an AWS S3 bucket object using Typescript in NextJS, allowing for easy file downloads on the client-side using @aws-sdk/S3Client

In the utilization of v3 of the S3Client, it appears that all existing examples are based on the old aws-sdk package. The goal is for the client (browser) to access a file from S3 without revealing the key from the backend. From my research, it seems tha ...

What is the best way to transfer information from one column to another column with Office Scripts?

I'm in the process of automation with Microsoft Excel using Office Scripts, and I've hit a roadblock when trying to transfer data from one column to another. https://i.sstatic.net/56ipi.png Specifically, I need to move the data from the Date co ...

Declaring Typescript modules across multiple .d.ts files

If my original .d.ts definition file is like this: main.d.ts: declare module myMod { } Now, let's say I want to separate out the security definitions into another file but keep them under the same module. Here's what I'm thinking: main. ...

What methods can I use to analyze the integrity of the data's structure?

Currently working on an API using NestJS and typeorm. I am in need of a way to verify the format of the data being returned to clients who make requests to it. For instance, when accessing the /players route, I expect the data to have a specific structure ...

How can contextual binding be implemented in TypeScript?

In Laravel, you have the option to specify which class should be imported if a particular class is accessed. For example, if someone tries to use the Filesystem contract, it will return the Storage Facade (Laravel Contextual Binding). Similarly, if someo ...

Can components be SSGed individually rather than entire pages?

I am currently working with Next.js and I am wondering if there is a way to statically generate and display the database values in the header and footer components used across all pages. While getStaticProps can generate pages statically, it doesn't ...

Trouble with Typescript in VSCode made easy

Setting up a VSCode environment for working with TypeScript v2.03 has been challenging. Beginning with a simple vanilla javascript snippet that can be tested in node via the integrated terminal. function Person() { this.name = ""; } Person.prototy ...