SmujMaiku
SmujMaiku

Reputation: 642

Defining a mixed array of tuples

I have a function I'm trying to convert to typescript. It deals with an Array of mixed type tuples similar to Promise.all. I can't figure out how to setup the definitions for the function.

export type Type<T> = [T, boolean];

function fn<T1, T2>(list: [Type<T1>, Type<T2>]): [T1, T2] {
    return list.map(([value]) => value);
}

fn([
    ['string', true],
    [123, false],
    // Probably more
]);
// [ 'string', 123 ]

Promise.all defines ten different options like:

all<T1, T2, T3>(values: readonly [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>]): Promise<[T1, T2, T3]>;
all<T1, T2>(values: readonly [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>]): Promise<[T1, T2]>;
all<T>(values: readonly (T | PromiseLike<T>)[]): Promise<T[]>;

Do we have a better way in typescript than explicitly listing all the possible lengths of the array?

Upvotes: 0

Views: 569

Answers (2)

jcalz
jcalz

Reputation: 329443

I'd be inclined to make the list parameter a mapped array/tuple type; if the desired output of the function is some array/tuple type T, then list is what you get when you iterate over each numeric index I of T and change the element from T[I] to Type<T[I]>. Then the compiler can use inference from mapped types to compute T from the type of list.

Here's one possible implementation:

export type Type<T> = readonly [T, boolean];

function fn<T extends readonly any[]>(
  list: readonly [...{ [I in keyof T]: Type<T[I]> }]
): T {
    return list.map(([value]) => value) as any;
}

Of note:

  • I've made all array types readonly because this is less restrictive than mutable arrays. Mutable array types are assignable to read-only array types, but not vice-versa. Nothing in the example code leads me to believe you actually want to prohibit read-only inputs, and certain language features like const assertions produce them, so you'll probably be happier this way.

  • The list parameter is of a variadic tuple type [...XXX] instead of just XXX; this gives the compiler a hint that it should be interpreting list as a tuple and not an unordered array. Otherwise you'd lose the ordering, and the output of fn() would tend to be something like Array<string | number> instead of the [string, number] or ["string", 123] you're looking for.

  • I have used a type assertion to any in the implementation of fn(). While you could possibly avoid any and use something less unsafe, you will never get full type safety here; the compiler does not understand that list's map() method will behave as the call signature requires. The TS library typing for map() returns an unordered array type: it would infer that you're outputting a value of type T[number][] (an unordered array of the elements of T). We have to use a type assertion or other similar loosening to tell the compiler that it's okay if it can't verify that the output will be of type T, and that we are taking on the responsibility for verifying type safety. This means we should be careful that the implementation is correct, because the compiler will not notice if we get it wrong.


Okay, let's test it:

const result = fn([
    ['string', true],
    [123, false],
    [new Date(), true]
]);
// const result: [string, number, Date]

Okay, the compiler sees that the output is of type [string, number, Date]. If that's sufficient, great. If you wanted to see string literal or numeric literal types instead, you could use a const assertion:

const resultNarrower = fn([
    ['string', true],
    [123, false],
    [new Date(), true]
] as const);
// const resultNarrower: ["string", 123, Date]

This is accepted because of the readonly changes above. The output type is now ["string", 123, Date]. Hooray!


Playground link to code

Upvotes: 1

You might be interested in next solution:


type Fst<T> = T extends readonly [infer A, infer B] ? A : never;
type MapPredicate<T> = Fst<T>

type Mapped<
    Arr extends ReadonlyArray<unknown>,
    Result extends ReadonlyArray<unknown> = readonly []
    > = Arr extends readonly []
    ? readonly [1]
    : Arr extends readonly [infer H]
    ? readonly [...Result, MapPredicate<H>]
    : Arr extends readonly [infer Head, ...infer Tail]
    ? Mapped<readonly [...Tail], readonly [...Result, MapPredicate<Head>]>
    : Readonly<Result>;

type Result = Mapped<[['str', true], [123, true]]>; // ["str", 123]


export type Type<T> = readonly [T, boolean];

function fn<T extends ReadonlyArray<readonly [unknown, boolean]>>(list: T) {
    // no way to avoid type casting here
    return list.map(([value]) => value) as unknown as Mapped<T>
}

const arr = [
    ['string', true],
    [123, false],
    // Probably more
] as const;

const result = fn(arr); // readonly ["string", 123]

Upvotes: 0

Related Questions