5

I am trying to create a mapped tuple that drills down to the type of a member:

export type Argument<T = unknown> = {
  value: T;
};

export type TypesOf<T extends Argument[]> = {[Index in keyof T]: T[Index]["value"]};

So, TypesOf<[{value: number}, {value: string}]> should yield [number, string].

However, I get this error:

Type '"value"' cannot be used to index type 'T[Index]'.

EDIT:

Bonus question after applying @jcalz's solution:

const tuple = [{value: 1}, {value:"foo"}] as const;
type V = TypesOf<typeof tuple>;

I get error:

Type 'readonly [{ readonly value: 1; }, { readonly value: "foo"; }]' does not satisfy the constraint 'Argument<unknown>[]'.
  The type 'readonly [{ readonly value: 1; }, { readonly value: "foo"; }]' is 'readonly' and cannot be assigned to the mutable type 'Argument<unknown>[]'.
user3612643
  • 5,096
  • 7
  • 34
  • 55
  • Note that this question as stated doesn't spell out what the problem is: presumably it's the error I mention in my answer but it would be nice to know this for sure. – jcalz Oct 25 '21 at 14:36
  • That was eaten... sorry, will add... – user3612643 Oct 25 '21 at 14:44

1 Answers1

4

The TypesOf implementation you present works, in that TypesOf<[{value: number}, {value: string}]> does indeed evaluate to [number, string]. Mapped types on arrays an tuples result in arrays and tuples.

But there is an issue, reported at microsoft/TypeScript#27995 where the compiler does not realize that inside the mapped type implementation {[I in keyof T]: ...T[I]...} that I will only end up being the numeric-like indices. It thinks that maybe I will be things like "push" and "pop", and so T[I] cannot be assumed to be of type Argument:

export type TypesOf<T extends Argument[]> =
    { [I in keyof T]: T[I]["value"] };
// -----------------> ~~~~~~~~~~~~~
// Type '"value"' cannot be used to index type 'T[I]'.

Presumably this is why you asked this question in the first place. The GitHub issue is listed as a bug, but it's been open for a long time and it's not clear if anything will happen here.


In cases like these I tend to use the Extract<T, U> utility type to help convince the compiler that some type will be assignable to another. If you have the type T that you know will be assignable to U, but the compiler does not, you can use Extract<T, U> in place of T. When T is specified later, Extract<T, U> will evaluate to just T if you are right about the assignability. And Extract<T, U> will be seen by the compiler to be assignable to both T and U.

In our case, we know T[I] will be assignable to Argument but the compiler doesn't. So the following workaround using Extract<T[I], Argument> will suppress the compiler error without affecting the output of TypesOf:

export type TypesOf<T extends Argument[]> =
    { [I in keyof T]: Extract<T[I], Argument>["value"] }; // no error

type Z = TypesOf<[{ value: number }, { value: string }]>;
// type Z = [number, string]

Looks good!

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • Thanks a lot! I added a BONUS QUESTION re using your solution with the typeof an actual tuple const. – user3612643 Oct 25 '21 at 14:53
  • Change your constraint to `T extends readonly Argument[]` instead of `T extends Argument[]`. The former is more accepting than the latter (`Argument[]` is assignable to `readonly Argument[]` but not vice versa). "Bonus questions" are kind of a no-no, though, since they are scope creep and you would probably be better off asking as a separate post (after searching for existing questions, of course). – jcalz Oct 25 '21 at 15:03
  • Thanks, got it! I changed it to `export type TypesOf = {[Index in keyof T]: TypeOf>};` to force passing tuples. – user3612643 Oct 25 '21 at 15:04