Raghav Mehta
Raghav Mehta

Reputation: 203

TypeScript TupleIndexed type and enforce read only for arrays passed in as arguments

I want to create type in TypeScript that takes a type parameter T and tuple/ReadonlyArray of keyof T and returns a ReadonlyArray of the keys indexed into T.

type TupleIndexed<T, K extends ReadonlyArray<keyof T>> = {
  [C in keyof K]: T[C];
};

But I get Type 'C' cannot be used to index type 'T'.

One workaround I found is

type TupleIndexed<T, K extends ReadonlyArray<keyof any>> = {
  [C in keyof K]: K[C] extends keyof T ? T[K[C]] : never;
};

While it gets the job done, I don't understand why the conditional is necessary for the compiler.

This way I'm able to write typed functions that preserve the positional type information like the following

function pluck<T, K extends ReadonlyArray<keyof T>>(obj: T, keys: K): TupleIndexed<T, K> {
  return keys.map(key => obj[key]);
}

const vals = pluck({name: 'John', age: 25, adult: true}, [
  'name', 
  'age', 
  'adult'
] as const);
const name = vals[0]; // string
const age = vals[1]; // number
const adult = vals[2]; // boolean
const doesNotExist = vals[3]; // ERROR

However, even in this solution, not casting the array as const still compiles

function pluck<T, K extends ReadonlyArray<keyof T>>(obj: T, keys: K): TupleIndexed<T, K> {
  return keys.map(key => obj[key]);
}

const vals = pluck({name: 'John', age: 25, adult: true}, [
  'name', 
  'age', 
  'adult'
]);  // Not explicitly stating as const still compiles
const name = vals[0]; // string | number | boolean
const age = vals[1]; // string | number | boolean
const adult = vals[2]; // string | number | boolean
const doesNotExist = vals[3]; // string | number | boolean

Which loses all the positional type safety. Is there a way to automatically cast the array as const or otherwise have the compiler throw an error when it isn't casted as const?

Upvotes: 2

Views: 98

Answers (1)

lukasgeiter
lukasgeiter

Reputation: 153030

Is there a way to automatically cast the array as const or otherwise have the compiler throw an error when it isn't casted as const?

As far as I know, there is not.

However, using a rest parameter instead of an array will result in TypeScript treating it as a tuple instead of an array. Of course this also means have to call it slightly differently:

type TupleIndexed<T, K extends ReadonlyArray<keyof any>> = {
  [C in keyof K]: K[C] extends keyof T ? T[K[C]] : never;
};

function pluck<T, K extends ReadonlyArray<keyof T>>(obj: T, ...keys: K): TupleIndexed<T, K> {
//                                                          ^^^
  return keys.map(key => obj[key]) as any;
}

const vals = pluck({name: 'John', age: 25, adult: true}, 'name', 'age', 'adult');
const name = vals[0]; // string
const age = vals[1]; // number
const adult = vals[2]; // boolean
const doesNotExist = vals[3]; // error

Playground

Upvotes: 1

Related Questions