What are some ways to utilize tuples in TypeScript along with generics?

My mission is to create a type safe mapping object where I can define key/value pairs just once.

I've managed to achieve this with the code below:

const myPropTuple = [
    [0, "cat"],
    [1, "dog"],
    [2, "bird"]
] as const;

type TMyPropKey = TInnerTupple<typeof myPropTuple>[0];
type TMyPropValue = TInnerTupple<typeof myPropTuple>[1];

function getMyProp(val: number) {
    type TKey = TInnerTupple<typeof myPropTuple>[0];

    const mapA2 = new Map(myPropTuple);
    if(!mapA2.has(val as TKey)) throw new RangeError("unexpected value");
    return mapA2.get(val as TKey);
}

// helper library (not to be inlined)
type TTupleType<T extends Iterable<any>> = T extends ReadonlyArray<infer R> ? R :never;
type TInnerTupple<I extends Iterable<any>> = TTupleType<I>;

// Tests
console.assert(getMyProp(1) === "dog");
//console.assert(getMyProp(1) === "turle");    // throws compiler error

const a: TMyPropValue = "cat";
//const b: TMyPropValue = "eagle";    // throws compiler error

Now, I want to make the function generic while still maintaining type safety:

The objective is to write

const myPropTuple = [
    [0, "cat"],
    [1, "dog"],
    [2, "bird"]
] as const;

console.assert(getGenericProp(myPropTuple, 1) === "dog");

const yourPropTuple = [
    [0, "fish"],
    [1, "towel"],
    [2, "whale"]
] as const;

console.assert(getGenericProp(yourPropTuple, 0) === "fish");

and ensure that the following examples fail to compile

console.assert(getGenericProp(myPropTuple, 1) === "turle");    // throws compiler error

type TYourPropValue = TInnerTupple<typeof yourPropTuple>[1];
const y: TYourPropValue = "dog";    // throws compiler error

addendum


@jcalz proposed an alternative solution which focuses on simplicity, so here is a slightly expanded version:

const animalProps = {
    0: "cat",
    1: "dog",
    2: "bird"
  } as const;

function getGenericProp2<T extends object>(props: T, val: keyof T): T[keyof T] {
    const found = props[val];
    if(found === undefined) throw new RangeError("unexpected value");
    return found;
}
type TValues = ValueOf<typeof animalProps>;
type TKeys = keyof typeof animalProps;

Answer №1

Your solution could be implemented as follows:

function fetchGenericProperty<T extends readonly [any, any]>(
  propPairArray: readonly T[], targetValue: T[0]
) {
  const pairMap: Map<T[0], T[1]> = new Map(propPairArray);
  if (!pairMap.has(targetValue)) throw new RangeError("unexpected value");
  return pairMap.get(targetValue);
}

This function is generic and expects the elements of `propPairArray` to consist of a union of readonly 2-tuples. The type of `targetValue` is specified as `T[0]`, representing the first element in each tuple. The return type can be either `T[1]` or `undefined`. This accounts for the fact that Map.get() may return `undefined`, even after checking with `has()` (see Why `map.has()` doesn't act as a type guard).

You can test this function by running a few cases:

const animals = [
  [0, "cat"],
  [1, "dog"],
  [2, "bird"]
] as const;
console.assert(fetchGenericProperty(animals, 1) === "dog"); // successful

const objects = [
  [0, "fish"],
  [1, "towel"],
  [2, "whale"]
] as const;
console.assert(fetchGenericProperty(objects, 0) === "fish"); // successful

console.assert(fetchGenericProperty(animals, 1) === "turtle"); // error!
// This comparison seems unintentional due to incompatible types.

fetchGenericProperty(animals, 3) // error!
// Argument '3' cannot be assigned to type '0 | 2 | 1'.

All tests passed successfully.


Note that the function does not determine which specific output corresponds to a given input. While this could be accomplished, it is not suitable for use with `Map`. Alternatively, consider utilizing object indexing:

const obj = {
  0: "cat",
  1: "dog",
  2: "bird"
} as const;
const val = obj[1];
// const val: "dog"

In this case, no function is necessary for accessing values. However, this approach exceeds the scope of the original query.

Link to code 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

Struggling to integrate authentication and authorization features into a ReactJS application with Microsoft Azure AD or Database login functionality

We have an application built on React v18 with a backend that includes a Web API and SQL Server database. Our requirement is to authenticate and authorize users using either MS Azure AD or the database. If a user attempts to log in with a username and pas ...

NextJS API routes consistently provide a status code of 200 upon execution

I am new to the concepts of Next.js, and I recently encountered an issue while attempting to fetch data from an API. The API is designed to check if a user session exists (i.e., if the user is logged in) and then returns a JSON response through a GET reque ...

How can I incorporate TypeScript paths into Storybook with Webpack version 5?

Storybook includes a pre-configured Webpack setup. Up until Webpack v4, it was possible to utilize tsconfig-paths-webpack-plugin and define custom tsconfig (or any other plugin) like so: const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plug ...

Is there any way to deactivate the saved query in react-admin without having to develop a new component?

The latest version of react-admin, version 4, introduced a new feature that allows saving filters. I'm curious about how to disable this functionality without having to create an additional filter button. https://i.stack.imgur.com/uTrUe.gif ...

Angular version 7.2.1 encounters an ES6 class ReferenceError when attempting to access 'X' before it has been initialized

I have encountered an issue with my TypeScript class: export class Vehicule extends TrackableEntity { vehiculeId: number; constructor() { super(); return super.proxify(this); } } The target for my TypeScript in tsconfig.json is set to es6: ...

An issue was encountered during the prerendering of the page "/". For more information on this error, visit: https://nextjs.org/docs/messages/prerender-error Attempting to adjust the request (unable to determine

It's been four days and I'm still stuck. I've seen some suggestions to use axios and set the timeout or switch from HTTP to HTTPS when fetching data, but nothing has worked. I'm now four days behind deadline and the client is not going ...

Using React with an Array of Promises in Typescript

I have a function that looks like this: function queryProposals(hash:string) { let result = api?.query.backgroundCouncil.proposalOf( hash,(data1:any)=>{ let injectedData = data1.toPrimitive().args.account as InjectedAccou ...

Establishing the data type for the state coming from the API

Whenever I try to add a new API response to the status, it shows as undefined. I need to filter out the incoming data from randomcocktail and then put it to use. Random.tsx import { useState, useEffect } from "react"; import { CocktailType } ...

What are the steps to ensure a successful deeplink integration on iOS with Ionic?

Recently, I was working on a hybrid mobile app for Android/iOS using Nuxt 3, TypeScript, and Ionic. The main purpose of the app is to serve as an online store. One important feature involves redirecting users to the epay Halyk website during the payment pr ...

generating declaration files for components with accurate type definitions from propTypes

I am in the process of creating a react/js library using rollup. My approach involves utilizing typescript to generate declaration files for my components. Despite having propTypes defined for my components, I have noticed that the emitted .d.ts files as ...

Whenever I make a POST request to the API in Ionic 2, I encounter a "Connection refused (x192)" error

I have been struggling with an error in my code for the past two days and I can't seem to find a solution. Can someone please help me with this? The error message is as follows: [Error] WebSocket network error: The operation couldn’t be complet ...

Steps for removing the console warning message: "The use of enableRowSelect has been deprecated. Instead, please utilize rowSelection."

) I have integrated React Data Grid from https://adazzle.github.io/react-data-grid/ multiple times in my application. One thing I noticed is that there is a console warning related to a prop called "enableRowSelect" which indicates whether the prop is bein ...

Retrieve the value of a hidden input when a button is clicked using reactive forms in Angular

I am currently attempting to retrieve the values of hidden input fields that are dynamically added when the user clicks on the "insert more" button. If you'd like to view the code in action, you can visit this StackBlitz link: get hidden input value ...

Transferring information via Segments in Ionic 3

I'm facing a challenge where I need to transfer a single ion-card from the "drafts" segment to the "sent" segment by simply clicking a button. However, I am unsure of how to achieve this task seamlessly. Check out this image for reference. ...

Perplexed by the implementation of "require(...)" in TypeScript

After reading several blog posts, my understanding of TypeScript modules remains confusing. I have worked with three different modules (all installed via npm) and encountered varying behavior: (1) Importing and using Angular 2 from npm required me to add ...

Prevent users from clicking buttons until all mandatory fields are filled out using react-hook-form

I am seeking guidance on how to dynamically disable a button based on the input values of both Name and State in the given code snippet. Specifically, I want to restrict button functionality until both name and state fields are filled out, regardless of ...

Mastering the art of effectively capturing and incorporating error data

Is there a way to retain and add information to an Error object in typescript/javascript without losing the existing details? Currently, I handle it like this: try { // code that may throw an error } catch (e) { throw new Error(`Error while process ...

Tips for maintaining the menu state following a refresh

Is there a way to save the menu state when pressing F5? I'm looking for a similar functionality as seen on the Binance website. For example, clicking on the Sell NFT's submenu and then refreshing the page with F5 should maintain the menu state on ...

Sign up for the identical Observable within a Child Component in Angular 2 using TypeScript

This question may seem simple, but as a newcomer to Angular 2, I often find myself needing more explanation despite the good examples and tutorials available. Within a component, I have an observable that gets updated periodically. While I've simplif ...

The orderBy filter seems to be malfunctioning

I'm utilizing ngx-pipes from this webpage: https://www.npmjs.com/package/ngx-pipes#orderby Specifically, I am using the orderBy pipe. However, when I implement the orderBy pipe in my HTML, the information is not being ordered correctly (from minor t ...