Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Make a generic type Array<keyof T> require all keys of T

I would like to declare a type that requires all the keys of a given type T to be included in an array, e.g.:

checkKeys<T>(arr: Array<keyof T>): void {
  // do something
}

interface MyType {
  id: string;
  value: number;
}

Currently if a call checkKeys<MyType>, TS will consider the value passed as valid if it contains any key of MyType (id | value):

checkKeys<MyType>(['id', 'value']); // valid

checkKeys<MyType>(['id']); // valid

checkKeys<MyType>(['id', 'values']); // invalid

Is it possible to require that all keys are specified in the array?

like image 805
don Avatar asked Aug 26 '18 17:08

don


2 Answers

You can't do that with an array type (at least I am not aware of a way to spread the union of keys into a tuple type, there may be one I'm just not aware of it). An alternative would be to use an object literal to achieve a similar effect. The syntax is a bit more verbose but the compiler will validate that only the correct keys are specified. We will use the Record mapped type and we can use the 0 literal types for values as only the keys matter.

function checkKeys<T>(o: Record<keyof T, 0>): void {
     // do something
}

interface MyType {
    id: string;
    value: number;
}

checkKeys<MyType>({ id: 0, value: 0 }); // valid

checkKeys<MyType>({ id: 0 }); // invalid

checkKeys<MyType>({ id: 0, values: 0 }); // invalid
like image 56
Titian Cernicova-Dragomir Avatar answered Nov 16 '22 06:11

Titian Cernicova-Dragomir


I've found a workaround but actually the solution is not perfect:

interface MyType {
  id: string;
  value: number;
}
const myType: MyType = {
   id: '',
   value: 0
};
type ArrType<T> = Array<keyof T>;
function isMyTypeArr<T>(arg: any[]): arg is ArrType<T> {
  return arg.length === Object.keys(myType).length;
}

function checkKeys<T>(arr: ArrType<T>): void {
  if (isMyTypeArr(arr)) {
    console.log(arr.length);
    // some other stuff
  }
}
checkKeys<MyType>(['id', 'x']); // TS error
checkKeys<MyType>(['id']); // no console because of Type Guard
checkKeys<MyType>(['id', 'value']); // SUCCESS: console logs '2'

The idea is to create a simple object which implements the initial interface. We need this object in order to get its keys length for comparison in the isMyTypeArr Type Guard. Type Guard simply compare the length of arrays - if they have the same length, it means that you provide all properties.


Edit

Added another similar (more generic) solution - the main differences are:

  • use class with constructor params which implements the initial interface;
  • this class has length property (because basically it's a constructor function) we can use it in our Type Guard;
  • we also have to pass class name as a second parameter in order to get it constructor args length. We cannot use generic type T for this, because the compiled JS has all the type information erased, we can't use T for our purpose, check this post for more deta

So this is the final solution:

interface IMyType {
  id: string;
  value: number;
}
class MyType implements IMyType {
  constructor(public id: string = '', public value: number = 0) {}
}
type ArrType<T> = Array<keyof T>;
function isMyTypeArr<T>(arg: ArrType<T>, TClass: new () => T): arg is ArrType<T> {
  return arg.length === TClass.length;
}

function checkKeys<T>(arr: ArrType<T>, TClass: new () => T): void {
  if (isMyTypeArr<T>(arr, TClass)) {
    console.log(arr.length);
    // some other stuff
  }
}

checkKeys<MyType>(['id', 'x'], MyType); // TS error
checkKeys<MyType>(['id'], MyType); // no console because of Type Guard
checkKeys<MyType>(['id', 'value'], MyType); // SUCCESS: console logs '2'

Notice that these examples are based on TypeScript issue 13267

p.s. also created a stackblitz demo of both examples

like image 2
shohrukh Avatar answered Nov 16 '22 06:11

shohrukh