13

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?

shohrukh
  • 2,989
  • 3
  • 23
  • 38
don
  • 4,113
  • 13
  • 45
  • 70
  • It sounds like you should be using an [Enum](https://www.typescriptlang.org/docs/handbook/enums.html) if the value has to be of a specific *type*. – Erik Philips Aug 26 '18 at 22:38
  • @ErikPhilips an Enum is what I had in mind as well, but I still needed the interface `MyType`, so I wanted to avoid having duplicates. – don Sep 05 '18 at 13:31

2 Answers2

8

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
Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
2

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

shohrukh
  • 2,989
  • 3
  • 23
  • 38
  • 2
    I would use `Record`for the `myType` constant. If the interface has nested objects creating compatible values for each field may be difficult and the value is irrelevant to usage. Also your solution has the drawback of beeing a runtime check not a compile time one. – Titian Cernicova-Dragomir Aug 26 '18 at 20:18
  • @sherlock thank's for the very complete suggestion. it is indeed close to what I am doing right now, that's why I was looking for a solution for compile time :-) – don Sep 05 '18 at 13:34