Mapping strings bidirectionally in Typescript

I am currently working on a two-way string mapping implementation;

const map = {} as MyMap; // need the correct type here
const numbers = "0123456789abcdef" as const;
const chars   = "ghijklmnopqrstuv" as const;
for (let i = 0; i < numbers.length; ++i) {
  map[numbers[i] as GetChars<Numbers>] = chars[i] as GetChars<Chars>;
  map[chars[i] as GetChars<Chars>] = numbers[i] as GetChars<Numbers>;
}

The logic behind this code is straightforward, but I'm facing an issue with the MyMap type. Here's what I have attempted so far:

type Numbers = "0123456789abcdef";
type Chars = "ghijklmnopqrstuv";

type GetChars<T extends string> = T extends `${infer Left}${infer Right}` ? Left | GetChars<Right> : never;

type StringToTuple<T extends string> = T extends `${infer Left}${infer Right}` ? [Left, ...StringToTuple<Right>] : [];
type NumbersTuple = StringToTuple<Numbers>;
type CharsTuple = StringToTuple<Chars>;
type Index = Exclude<keyof NumbersTuple, keyof any[]>;

// TempMap implementation is incorrect
// Why does CharsTuple[P] always result in a union?
type TempMap = {
  [P in Index as NumbersTuple[Index]]: CharsTuple[P];
};

type Reverse<T extends { [index: string]: string }> = {
  [P in keyof T as T[P]]: P
};
type MyMap = TempMap & Reverse<TempMap>;

TempMap is not accurate, and I'm struggling to understand why CharsTuple[P] always ends up being a union.

Try it on Playground

Answer №1

Initially, it is valuable to create a distinct function for establishing two-way mapping. The desired behavior resembles how numerical enums operate in TypeScript. Nonetheless, for a generic function, both arguments' lengths should be validated.

Take this example:

type StringToTuple<T extends string> =
  T extends `${infer Left}${infer Right}`
  ? [Left, ...StringToTuple<Right>]
  : [];

// checks if number is a literal type or not
type IsLiteralNumber<N extends number> =
  N extends number
  ? number extends N
  ? false
  : true
  : false

{
  type _ = IsLiteralNumber<2> // true
  type __ = IsLiteralNumber<number> // false

}

/* To compare the length of both arguments, we need to ensure
 * that the length is a literal number and not just a "number" type
 * If it's a "number" type instead of "5" or "9", how can we compare it
 * at all?
 */
type IsLengthEqual<Fst extends string, Scd extends string> =
  IsLiteralNumber<StringToTuple<Fst>['length']> extends true
  ? IsLiteralNumber<StringToTuple<Scd>['length']> extends true
  ? StringToTuple<Fst>['length'] extends StringToTuple<Scd>['length']
  ? StringToTuple<Scd>['length'] extends StringToTuple<Fst>['length']
  ? true
  : false
  : false
  : false
  : false

{
  type _ = IsLengthEqual<'a', 'b'> // true
  type __ = IsLengthEqual<'a', ''> // false
  type ___ = IsLengthEqual<'', ''> // true
  type ____ = IsLengthEqual<'abc', 'abc'> // true

}

const numbers = "0123456789abcdef" as const;
const chars = "ghijklmnopqrstuv" as const;

const twoWayMap = <
  Hex extends string,
  Chars extends string
>(
  hex: Hex,
  chars: Chars,
  ...validation: IsLengthEqual<Hex, Chars> extends true ? [] : [never]
) => { }

twoWayMap(numbers, chars) // works
twoWayMap('a', 'aa') // fails

Next, we need to determine the return type by combining two strings using the Zip method to create a two-way dictionary. It's unnecessary to construct two-way binding in one utility type; let's focus on one-way binding only.

type List = ReadonlyArray<PropertyKey>

// unnecessary array methods such as forEach, map, concat, etc.
type RemoveArrayKeys<Tuple extends List> = Exclude<keyof Tuple, keyof PropertyKey[]>

type Merge<
  T extends List,
  U extends List
  > = {
    [Prop in RemoveArrayKeys<T> as T[Prop] & PropertyKey]: U[Prop & keyof U]
  }

{
  type _ = Merge<['a'], ['b']> //  { a: "b" };
  type __ = Merge<['a', 'b'], ['c', 'd']> //  { a: "c", b:"d" };
}

Now, creating a two-way binding becomes simple; just call Merge with reversed arguments:


type Zip<
  T extends List,
  U extends List
  > =
  Merge<T, U> & Merge<U, T>

type Result = Zip<StringToTuple<'ab'>, StringToTuple<'cd'>>

{
  type _ = Zip<StringToTuple<'ab'>, StringToTuple<'cd'>>['a'] // "c"
  type __ = Zip<StringToTuple<'ab'>, StringToTuple<'cd'>>['c'] // "a"
}

Complete code:


const NUMBERS = "0123456789abcdef";
const CHARS = "ghijklmnopqrstuv";

type StringToTuple<T extends string> =
  T extends `${infer Left}${infer Right}`
  ? [Left, ...StringToTuple<Right>]
  : [];


type IsLiteralNumber<N extends number> =
  N extends number
  ? number extends N
  ? false
  : true
  : false


type IsLengthEqual<Fst extends string, Scd extends string> =
  IsLiteralNumber<StringToTuple<Fst>['length']> extends true
  ? IsLiteralNumber<StringToTuple<Scd>['length']> extends true
  ? StringToTuple<Fst>['length'] extends StringToTuple<Scd>['length']
  ? StringToTuple<Scd>['length'] extends StringToTuple<Fst>['length']
  ? true
  : false
  : false
  : false
  : false

type List = ReadonlyArray<PropertyKey>

type RemoveArrayKeys<Tuple extends List> = Exclude<keyof Tuple, keyof PropertyKey[]>

type Merge<
  T extends List,
  U extends List
  > = {
    [Prop in RemoveArrayKeys<T> as T[Prop] & PropertyKey]: U[Prop & keyof U]
  }


type Zip<
  T extends string,
  U extends string
  > =
  & Merge<StringToTuple<T>, StringToTuple<U>>
  & Merge<StringToTuple<U>, StringToTuple<T>>

type Result = Zip<'ab', 'cd'>


function twoWayMap<
  Hex extends string,
  Chars extends string
>(
  hex: Hex,
  chars: Chars,
  ...validation: IsLengthEqual<Hex, Chars> extends true ? [] : [never]
): Zip<Hex, Chars>
function twoWayMap<
  Hex extends string,
  Chars extends string
>(
  hex: Hex,
  chars: Chars,
) {
  return hex.split('').reduce((acc, elem, index) => {
    const char = chars[index]
    return {
      ...acc,
      [elem]: char,
      [char]: elem
    }
  }, {})
}

const result = twoWayMap(NUMBERS, CHARS)

result['a'] // "q"
result["q"] // a

Playground

You can explore more about function argument type validation in my articles here and here.

In the above example, I employed overloading for return type inference.

If you're not fond of overloads, you can stick with this simpler version:

const twoWayMap = <
  Hex extends string,
  Chars extends string
>(
  hex: Hex,
  chars: Chars,
) =>
  hex.split('').reduce((acc, elem, index) => ({
    ...acc,
    [elem]: chars[index],
    [chars[index]]: elem
  }), {} as Zip<Hex, Chars>)

This method is perfectly valid. When using reduce, there is no other way to infer the return type of a function without employing as type assertion.

Answer №2

In my opinion, the most straightforward approach is to simultaneously iterate over both strings:

type Zip<
  StringA extends string,
  StringB extends string
> = StringA extends `${infer CharA}${infer RestA}`
  ? StringB extends `${infer CharB}${infer RestB}`
    ? { [key in CharA]: CharB } & { [key in CharB]: CharA } & Zip<RestA, RestB>
    : {}
  : {};

type MyMap = Zip<Numbers, Chars>;

Visit the Playground for this code snippet

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

Unable to associate ngModel because it is not recognized as a valid property of the "Component"

Currently, I am in the process of creating a custom form component using Angular 4. I have included all necessary components for ngModel to function properly, but unfortunately, it is not working as expected. Below is an example of my child component: ex ...

Can the return type of a function be utilized as one of its argument types?

When attempting the following code: function chain<A, B extends (input: A, loop: (input: A) => C) => any, C extends ReturnType<B>> (input: A, handler: B): C { const loop = (input: A): C => handler(input, loop); return loop(input) ...

Is it possible to capture and generate an AxiosPromise inside a function?

I am looking to make a change in a function that currently returns an AxiosPromise. Here is the existing code: example(){ return api.get(url); } The api.get call returns an object of type AxiosPromise<any>. I would like to modify this function so ...

TailwindCSS applies CSS settings from tailwind.admin.config.js without overriding tailwind.config.js. The @config directive is utilized for this purpose

I'm currently working on a project using Vite and React. I have a tailwind.admin.css file that is similar to the example provided in the documentation. @config './configs/tailwind.admin.config.js'; @tailwind base; @tailwind components; @tai ...

What is the procedure for linking the value (<p>John</p>) to the mat form field input so that it displays as "John"?

Can I apply innerHTML to the value received from the backend and connect it to the matInput? Is this a viable option? ...

The 'locale' parameter is inherently assigned the type of 'any' in this context

I have been using i18n to translate a Vue3 project with TypeScript, and I am stuck on getting the change locale button to work. Whenever I try, it returns an error regarding the question title. Does anyone have any insights on how to resolve this issue? ...

Showing json information in input area using Angular 10

I'm facing an issue with editing a form after pulling data from an API. The values I retrieve are all null, leading to undefined errors. What could be the problem here? Here's what I've tried so far: async ngOnInit(): Promise<void> ...

Is there a way to create a type interface that points to the structure of another type?

I am focused on developing a type interface that includes properties defined in another interface. Below is the schema definition for a model object in TypeScript: export interface ModelSchema { idAttribute: string; name: string; attributes: { ...

Exploring the integration of an Angular 4 application with Visual Studio 2017 using dot net core. Techniques for accessing configuration keys from appsetting.json in a TypeScript

I'm currently working on an Angular 4 application using Visual Studio 2017 with .NET Core. I need to figure out how to access configuration keys from appsetting.json in my TypeScript file. I know how to do it in the startup.cs file, but I'm strug ...

Guide on how to add a generic return type to a function in typescript

Is there a way to annotate a function that returns a factory in TypeScript to ensure it contains correct type definitions? Consider the following code: class item<T> { constructor(a: T) { this.a = a; } a: T } function generate(c) { ret ...

Users are reporting a problem with the PrimeNG confirmation dialog where it becomes unresponsive and locks up the screen

Previously functioning code seems to have been affected by an update to PrimeNG. The confirmation dialog that was once usable is now hidden behind a gray click-mask, rendering everything on the screen unclickable: https://i.sstatic.net/YN7Iu.png The HTML ...

Issue with rendering Base64 image array strings in FlatList component in React Native

In my RN App, I am trying to display a FlatList with Image Items but it seems like I have missed something. I am retrieving blob data from my API, converting it to a String using Buffer, and then adding it to an Array. This Array is used to populate the F ...

The TypeScript Type inside the Node Module Doesn't Seem to Be Functioning

In my React project, I am using the material-ui@next library along with typescript. Below is the code snippet that I have written: <CardMedia image={item.image_url} style={{ width: 238, height: 124.5 }} /> However, when I try to compile this code, ...

Can you conduct testing on Jest tests?

I am in the process of developing a tool that will evaluate various exercises, one of which involves unit-testing. In order to assess the quality of tests created by students, I need to ensure that they are effective. For example, if a student provides the ...

Angular 2: Emptying input field value on click event

I am experiencing an issue with resetting my input values. I have a search bar with filter functions. When I enter a value, it displays a list below and I want to add a function to these links. When I click on one of them, it takes me to another component ...

Express: issue retrieving numbers from request body array

JavaScript / TypeScript Issue: export const updateSettings = catchErrors(async (req, res) => { console.log("updateSettings req.body: ", req.body) const { organizationId, formdata, updatedItems, updateQuota } = req.body; console.lo ...

Error encountered: React Typescript does not support the "any" type in a template literal expression

I have been encountering an issue with my slider component in React Typescript. The error message I keep getting is related to the "Invalid type 'any' of template literal expression" specifically at the const fillWidth variable. I am struggling t ...

Is there a way to incorporate an "else" condition in a TypeScript implementation?

I am trying to add a condition for when there are no references, I want to display the message no data is available. Currently, I am working with ReactJS and TypeScript. How can I implement this check? <div className="overview-text"> < ...

The Microsoft EDGE browser is encountering a XHR HTTP404 error when trying to access a TypeScript

While debugging a Typescript web application in Visual Studio 2015 and using the Microsoft EDGE browser, an error is reported as follows: HTTP404: NOT FOUND - The server cannot locate anything that matches the requested URI (Uniform Resource Identifier). ...

Navigating the world of NestJs and TypeScript with the `mongoose-delete` plugin: a comprehensive guide

I am currently utilizing Mongoose within the NestJs library and I want to incorporate the mongoose-delete plugin into all of my schemas. However, I am unsure of how to implement it with nestJS and Typescript. After installing both the mongoose-delete and ...