Extracting types and returning a mapped tuple type

Viewed 108

I'm having issues with a mapped type converting to a union-type array rather than a tuple type.

I'm seeing an error that indicates that fn expects a signature of

'(...values: (string | boolean)[]) => void' vs my preferred '(a: string, b: boolean) => void'

It's likely my misunderstanding of this change, but is there something missing that would type RawValues<T> to a tuple rather than a union-type array to allow me to strongly type the arguments of the fn I pass in?

Thanks!

type ValueType<T> = {  value: T; };
type RawValue<T> = T extends ValueType<infer U> ? U : never;
type RawValues<T> = {
    [P in keyof T]: RawValue<T[P]>;
};
function extractAndRun<T extends ValueType<any>[]>(values: T, fn: (...values: RawValues<T>) => void): RawValues<T> {
    const rawValues = values.map((val) => val.value) as RawValues<T>;
    fn(...rawValues);
    return rawValues;
}

const result = extractAndRun(
  [{value: 'string'}, {value: true}],
  // PROBLEM IS HERE. See error message in code block below.
  (a: string, b: boolean) => {
    console.log(a);
    console.log(b);
  });
console.log(result); // ['string', true]
    Argument of type '(a: string, b: boolean) => void' is not assignable to parameter of type '(...values: (string | boolean)[]) => void'.
      Types of parameters 'a' and 'values' are incompatible.
        Type 'string | boolean' is not assignable to type 'string'.
          Type 'boolean' is not assignable to type 'string'.
    'b' is declared but its value is never read.

TS Playground here.

1 Answers

The problem is that Typescript weakens the type of your tuple to an array of unions, before it is used to infer T. There are two solutions: either ensure that the type is not weakened by using as const, or have a generic type which is inferred from the callback function's parameters instead of the array.

Solution 1: the upper bound of T is readonly to accommodate the as const assertion, and RawValues removes this modifier. Note that the result is inferred with literal types as ['string', true] instead of [string, boolean].

type RawValues<T> = {
    -readonly [P in keyof T]: RawValue<T[P]>;
};

function extractAndRun<T extends readonly ValueType<any>[]>(values: T, fn: (...values: RawValues<T>) => void): RawValues<T> {
    const rawValues = values.map((val: any) => val.value) as RawValues<T>;
    fn(...rawValues);
    return rawValues;
}

const result = extractAndRun([{value: 'string'}, {value: true}] as const, (a: string, b: boolean) => {
    // ...
});

Playground Link

Solution 2: T will be the array of raw values, so the WrappedValues type transforms it into the required tuple type.

type WrappedValues<T extends any[]> = T extends [infer A, ...infer B] ? [ValueType<A>, ...WrappedValues<B>] : []

function extractAndRun<T extends any[]>(values: WrappedValues<T>, fn: (...values: T) => void): T {
    const rawValues = values.map((val: any) => val.value) as T;
    // ...
}

Playground Link

Related