TypeScript Type Challenges in Real Projects

2024 · Managing complex key combinations in TypeScript.

Background

In my previousdata visualization project, I have a type issue.

This project is about spectral analysis. It has many electrodes with different labels, such as electrode A, B, C.

You can write a TypeScript type to constrain these keys:

type ElectrodeKeys = 'A'|'B'|'C'

But these electrodes can be combined in pairs. such as electeode A-B, A-C, B-C.

Let's add more electrode like this below:

type ElectrodeKeys = 'A'|'B'|'C'|'A-B'|'A-C'|'B-C'

However, as the number of keys increase, this type becomes hard to maintain.

Solution

What if this question is in JavaScript world?

The first solution is to use a double loop:

function generatePairs(arr:string[]) {
    const pairs = [];
    for (let i = 0; i < arr.length; i++) {
        for (let j = i + 1; j < arr.length; j++) {
            pairs.push(`${ arr[i]}-${ arr[j]}`);
        }
    }
    return pairs;
}

The second solution is to use recursive:

function generatePairs(arr:string[]) {
    const pairs:string[] = []

    function recursiveGenerate(currentIndex:number, currentPair:string[]) {
        if (currentPair.length === 2) {
            pairs.push(currentPair.join('-'));
            return;
        }

        for (let i = currentIndex; i < arr.length; i++) {
            recursiveGenerate(i + 1, [...currentPair, arr[i]]);
        }
}
recursiveGenerate(0, []);
return pairs;
}

It's easy to understanding the idea, in this type issue, we can use TypeScript generics to achieve.

type type ElectrodeKeys = ["A", "B", "C"]ElectrodeKeys = ['A','B','C']
type type CombKeys<T extends string[]> = T extends [infer F extends string, ...infer R extends string[]] ? `${F}-${R[number]}` | CombKeys<R> : neverCombKeys<function (type parameter) T in type CombKeys<T extends string[]>T extends string[]> =
 function (type parameter) T in type CombKeys<T extends string[]>T extends [infer function (type parameter) FF extends string, ...infer function (type parameter) RR extends string[]] 
      ? `${function (type parameter) FF}-${function (type parameter) RR[number]}` | type CombKeys<T extends string[]> = T extends [infer F extends string, ...infer R extends string[]] ? `${F}-${R[number]}` | CombKeys<R> : neverCombKeys<function (type parameter) RR> 
      : never
type type CombElectrodeKeys = "A-B" | "A-C" | "B-C"CombElectrodeKeys = type CombKeys<T extends string[]> = T extends [infer F extends string, ...infer R extends string[]] ? `${F}-${R[number]}` | CombKeys<R> : neverCombKeys<type ElectrodeKeys = ["A", "B", "C"]ElectrodeKeys>
  • T extends string[] means that the generic to accpet a string array.
  • Further check if T is an array with at least on element. If so, it destructures the first element as F and the rest as R.
  • CombKeys<R>: This is a recursive call to CombKeys to handle the remaining elements R.
  • : never:if T is not an array , the result is never.