What techniques can be used to determine which exact key was matched by a generic?

I am trying to find a method to deduce a more general string type key from a specific string that can be associated with it.

type Foo = {
  [x: `/tea/${string}/cup`]: void;
  [x: `/coffee/${string}/time`]: void;
  [x: `/cake/${string}/tin`]: void;
}

type MatchesFooKey<T extends string> = T extends keyof Foo ? true : false;
type TestMatch = MatchesFooKey<'/coffee/123/time'>; // Returns true as expected

type InferFooKey<T extends string> = T extends (infer R extends keyof Foo) ? R : never;
type TestInfer = InferFooKey<'/coffee/123/time'>; // Returns '/coffee/123/time', whereas I would prefer to get `/coffee/${string}/time` instead

For example, when presented with '/coffee/123/time', my goal is to infer back `coffee/${string}/time` according to the structure above.

Currently, infer R does not indicate which particular key it matched because the inference types are not aligned with what I am aiming for. Essentially, I wish to derive a specific key from Foo and store it in R.

Is there a technique to infer the broader type based on the specific one?

Take a look at this Typescript playground link.

Answer №1

Basically, what you're seeking is a modified version of the Extract<T, U> utility type that retrieves any item from the union T which serves as a superior type to U, rather than a subclass of U. In this way,

ExtractSubtype<keyof Foo, T>
will consist of any element within keyof Foo where T can be assigned to it, and not vice versa.

The definition of Extract is available at this link:

type Extract<T, U> = T extends U ? T : never;

This represents a distributive conditional type that disassembles a union T into its constituent parts, and assesses each part for assignment compatibility with U.

An implementation of ExtractSubtype<T, U> would look like this:

type ExtractSubtype<T, U> =
  T extends any ? [U] extends [T] ? T : never : never;

In order to preserve distribution over T, we use T extends ⋯ ? ⋯ : ⋯. Although there isn't a need to actually 'check' T, so we establish a condition that always evaluates as true, such as T extends any or T extends unknown or T extends T. Therefore, we employ T extends any ? ⋯ : never to distribute across T.

The core evaluation is on [U] extends [T] ? T : never, verifying whether the type U can be assigned to the specific member of

T</code under consideration. The bracket notation <code>[]
is used here to disable distributivity, as we wish to maintain U as-is without breaking it down into individual union elements.

With this insight, we can derive:

type InferFooKey<U extends string> = ExtractSubtype<keyof Foo, U>

type TestInfer = InferFooKey<'/coffee/123/time'>;
//   type TestInfer = `/coffee/${string}/time`^?

seems promising.


If the goal is to accomplish most of this in-line to avoid reusing ExtractSubtype, then consider this approach:

type InferFooKey<U extends string, T = keyof Foo> =
  T extends any ? [U] extends [T] ? T : never : never;

type TestInfer = InferFooKey<'/coffee/123/time'>;
//  type TestInfer = `/coffee/${string}/time`^?

Here, I've leveraged a default generic type argument to ensure that T remains consistent with keyof Foo, enabling T extends any ? ⋯ : never to operate correctly. Without this default setting, a direct check like

keyof Foo extends any ? ⋯ : never
wouldn't distribute (due to the requirement of checking a generic type parameter for distributive conditional types).

To extend distribution across unions within U, modify the code as follows:

type InferFooKey<U extends string, T = keyof Foo> =
  T extends any ? U extends T ? T : never : never;

The outcome depends on your desired behavior for InferFooKey<U> when dealing with a union.

type Test2 = InferFooKey<'/tea/abc/cup' | '/coffee/def/time'>;
// never? since no key is assignable to that union... or
// `/tea/${string}/cup` | `/coffee/${string}/time`

Either method is suitable for the provided example scenario.

Refer to this Playground link to code

Answer №2

Unsure of the exact implementation in raw TypeScript, but if you are open to utilizing external libraries, here is a method using hotscript

import { Pipe, Tuples, Unions, Fn } from 'hotscript';

type Foo = {
  [x: `/tea/${string}/cup`]: void;
  [x: `/coffee/${string}/time`]: void;
  [x: `/cake/${string}/tin`]: void;
}

type Route = keyof Foo;

interface RouteMatcher<T> extends Fn {
  return: T extends this["arg0"] ? true : false;
}

type InferFooKey<T extends string> => Pipe<Route,
  [
    Unions.ToTuple,
    Tuples.Filter<RouteMatcher<T>>,
    Tuples.ToUnion,
  ]>;
type TestInfer = InferFooKey<'/coffee/123/time'>; // /coffee/${string}/time

Playground

Answer №3

By leveraging TypeScript, you can achieve your desired outcome seamlessly. Here's the solution:

type Key = `/tea/${string}/cup` | `/coffee/${string}/time` | `/cake/${string}/tin`
type Foo = {
  [K in Key]: void;
}

type MatchesFooKey<T extends string> = T extends keyof Foo ? true : false;
type TestMatch = MatchesFooKey<'/coffee/123/time'>; // Outputs true as expected

type InferFooKey<T extends string> = T extends string & Key ? Key : never;

type Baz<Str extends string, Template extends string> = 
  (Template extends infer S extends string
    ? Str extends `${string}${infer R extends S}`
      ? R
      : never
    : never
  ) extends infer T ? [T] extends [never] ? '' : T : never;

type Qux = Baz<'/coffee/123/time', Key>;

Let's break it down starting from here.

type InferFooKey<T extends string> = T extends (infer R extends keyof Foo) ? R : never;
type TestInfer = InferFooKey<'/coffee/123/time'>; // Outputs '/coffee/123/time', aiming for `/coffee/${string}/time` instead

The objective is to get /coffee/${string}/time, not the exact value.

To address this, let's simplify the initial part like this:

type Key = `/tea/${string}/cup` | `/coffee/${string}/time` | `/cake/${string}/tin`
type Foo = {
  [K in Key]: void;
}

Now, we can introduce a straightforward alternative that functions effectively: We first verify if the value aligns with each unique value of the Key template literal:

  • If there's a match, we return it;
  • If not, we proceed to the next potential value of the template literal;

This approach leads us to the following implementation:

type Bar<T extends Key> = T extends `/tea/${string}/cup` ? `/tea/${string}/cup` : 
  T extends `/coffee/${string}/time` ? `/coffee/${string}/time`:
    T extends `/cake/${string}/tin` ? `/cake/${string}/tin` :
      never;

While this works, managing 1000 options this way becomes impractical. Hence, we require distribution behavior for scalability.

type Baz<Str extends string, Template extends string> = 
  (Template extends infer S extends string
    ? Str extends `${string}${infer R extends S}`
      ? R
      : never
    : never
  ) extends infer T ? [T] extends [never] ? '' : T : never;

What's happening here?

  • We establish the type Baz mapped based on our string's value, Str, and the possible template options Template (hence creating the type
    Key</code initially, replaceable with <code>keyof Foo
    ).
  • We ensure that whatever the Template's value, it extends string. To reference it later, we form the "variable" S.
  • We deconstruct our Str into a string extending
    S</code, generating the "variable" <code>R
    to derive the chosen option.
  • If both conditions are met, we return
    R</code, inferred to be the specific option of our template literal <code>Template</code matching the provided string <code>Str
    .
  • If either condition fails, indicating no match among the template literal's feasible values, we return never (twice due to two checks).

Lastly, execute:

type Qux = Baz<'/coffee/123/time', Key> // outputs `/coffee/${string}/time` 

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

Errors can occur when using TypeScript recursive types

Below is a simplified version of the code causing the problem: type Head<T> = T extends [infer U,...unknown[]] ? U : never; type Tail<T> = T extends [unknown,...infer U] ? U : []; type Converter = null; type Convert<T, U extends Converter& ...

What is the best way to bring a file from a different directory that is located on the same level?

Is there a way to import content from a file located in another directory at the same level? For instance, if I am working with file 1 in folder 1 and need to import information from file 2 in folder 2, how can this be achieved? I am encountering an error ...

The parameter of type 'void' cannot be assigned to the parameter of type 'PathParams'

Established the route handler and encountered an issue while integrating it into my route. import {Application, NextFunction} from 'express'; import {container} from 'tsyringe'; const routeConstantsArray = { }; const constants: any ...

Accessing JSON data stored locally and initializing it into a TypeScript variable within a React application

I'm new to working with JSON arrays and I'm facing a challenge. I am looking for a way to load data from a JSON file into a Typescript variable so that I can perform a specific operation that involves arrays. However, I'm unsure of how to ac ...

Issue in VueJs where mutations do not properly save new objects to the state

I am facing an issue with updating my vuex store after modifying my user credentials in a component. Below is the code snippet for reference: mutations: { updateUserState: function(state, user) { state.user = user; }, } actions: { updat ...

Struggling to assign the correct data type to a property in Typescript

I am encountering a TypeScript error with the following code. In this interface, the 'items' property can be either an object or an array depending on the code and response. I am unsure how to specify the datatype of 'array/object/any', ...

How can I extract just the initial 2 letters of a country name using AmCharts maps?

Having trouble with Amcharts maps. I have a map that displays countries as United States, but I only want them to show as US. Is there a country formatter available for this issue? Any help is appreciated. ...

Unable to retrieve device UUID using capacitor/device on Android

I'm currently attempting to obtain the UUID of my devices so that I can send targeted notifications via Firebase. My front end and back end are connected, enabling the back end to send notifications to the front end using Firebase. However, all I am a ...

`"Type is invalid" error occurring with component after importing it into a different project``

I am currently working on developing a custom Storybook 7 Typescript component library with React. I have successfully imported this library into another project using a private NPM package. However, one of the components in the library, specifically the ...

The Node.js express seems to be unable to fetch the css and js files

Sharing my main.ts file in which I am facing issues with linking my css and js files. view image import express from 'express'; import {Request,Response} from 'express'; import expressSession from 'express-session'; import pat ...

Guide to mocking the 'git-simple' branchLocal function using jest.mock

Utilizing the simple-git package, I have implemented the following function: import simpleGit from 'simple-git'; /** * The function returns the ticket Id if present in the branch name * @returns ticket Id */ export const getTicketIdFromBranch ...

Sorting through a list of strings by checking for a specific character within each string

After spending years dabbling in VBA, I am now delving into Typescript. I currently have an array of binary strings Each string represents a binary number My goal is to filter the array and extract all strings that contain '1' at position X I ...

The proper order for logging in is to first validate the login credentials before attempting

I created a custom validation class to verify if a user is logged in before allowing them access to a specific page. However, after implementing this validation, my program no longer routes me to the intended component. Validation.ts export class UserVal ...

Limiting the parameter type in Node.js and TypeScript functions

Currently working on a Node.js project utilizing TypeScript. I'm attempting to limit the argument type of a function to a specific base class. As a newcomer to both Node and TypeScript with a background in C#, I may not fully grasp some of the langua ...

Having trouble locating the 'tsc-watch' module due to the missing 'typescript/bin/tsc' when running the TypeScript compiler

Encountering the error "Cannot find module 'typescript/bin/tsc' when attempting to run tsc-watch yarn tsc-watch --noClear -p tsconfig.json yarn run v1.22.19 $ /Users/jason/Work/VDQ/VDQApp/node_modules/.bin/tsc-watch --noClear -p tsconfig.json Ca ...

Changing a d3 event from JavaScript to Typescript in an Angular2 environment

I am a beginner in Typescript and Angular 2. My goal is to create an Angular2 component that incorporates a d3js tool click here. However, I am facing challenges when it comes to converting it to Typescript. For instance, I am unsure if this code rewrite ...

Jasmine-ts is encountering a problem related to a subpath within a package, causing

Embarking on a journey of jasmine unit testing, I am encountering challenges when trying to run jasmine and locate my TypeScript written specs. Using jasmine-ts, I typically execute the following command: jasmine-ts --config=spec/support/jasmine.json The ...

In which situations is it required to specify the return type of a function in TypeScript?

When it comes to making functions in typescript, the language can often infer the return type automatically. Take for instance this basic function: function calculateProduct(x: number, y: number) { return x * y; } However, there are scenarios where dec ...

Leveraging getStaticProps in Next.js

I am currently embarking on my inaugural Nextjs project, focused on developing a basic blog utilizing the JSON placeholder API. Strangely, I am encountering an issue where the prop "posts" is being perceived as undefined. Can anyone provide assistance with ...

Issue: Encounter of "Uncaught (in promise) TypeError" while implementing RiveScript chatbot in Angular

I've been working on integrating a chatbot based on RiveScript into Angular. The chatbot is functioning well - I always see the correct response in the console. Displaying the user's input is also working seamlessly. However, I'm encounterin ...