Automatically decide array element return type based on array value for each index

I want to create a function with array of literal string from available key.

If first index using SECOND_KEY, in that index it must return that SECOND_TYPE type, and so on.

The code is below, and I provide comments to elaborate more:

type TestType = {
  FIRST_KEY: number,
  SECOND_KEY: string
}

function TestFunction<keyType extends (keyof TestType)[]>(keys: keyType) {
  return await AsyncStorage.multiGet(keys)

  /*
    from multiGet, probably the data will be like this

    [
      2,
      "Test string",
      2
    ]
  */
}

let arr = TestFunction(['FIRST_KEY', 'SECOND_KEY', 'FIRST_KEY'])

// arr[0] should have type number, and be able to access number methods like .toPrecision
// arr[1] should have type string, and be able to access string methods like .length
// arr[2] should have type number, and be able to access number methods like .toPrecision

Answer

By default Typescript will interpret your generic type parameter as a normal array ("FIRST_KEY" | "SECOND_KEY")[] which can have any length and can have these values in any order.

We need to force Typescript to interpret it as a fixed tuple type. We can do that by adding as const when we call the function, and by adding readonly to the generic in order to allow constant tuples which are readonly.

We need a mapped type to convert our tuple of keys to a tuple of values. Mapped types can be applied directly to tuples.

Within the body of the function, you will need to assert correctness with as because calling methods like .map on your function will cause it to be seen as normal array again.

Your actual use case regards AsyncStorage/localStorage where the value that you get will always be a string. You will need to convert it to a number with parseFloat in certain cases. This is a run-time action and cannot be done just with types. Your code needs to know which variables it should convert. Here we only have two keys, so we can deal with it on an if/then basis, but you might need to rethink how you handle casting in your actual app.

type TestType = {
    FIRST_KEY: number,
    SECOND_KEY: string
}

type TestValue<T> = {
    [K in keyof T]: T[K] extends keyof TestType ? TestType[T[K]] : never;
}

function TestFunction<keyType extends readonly (keyof TestType)[]>(keys: keyType): TestValue<keyType> {
    return keys.map(key => {
        const raw = localStorage.getItem(key); // is either string or null
        if (key === "FIRST_KEY") {
            return parseFloat(raw ?? "");
        } else {
            return raw ?? "";
        }
    }) as unknown as TestValue<keyType>;
}

let arr = TestFunction(['FIRST_KEY', 'SECOND_KEY', 'FIRST_KEY'] as const)
const [a, b, c] = arr; // types a: number, b: string, c: number

Typescript Playground Link

Leave a Reply

Your email address will not be published. Required fields are marked *