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?
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
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:
length
property (because basically it's a constructor function) we can use it in our Type Guard;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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With