Description
Suggestion
Something that I've needed from time to time is a way to check that an array contains all the keys on an interface. Right now I can use (keyof SomeType)[]
to verify that there are no typos/unknown keys, but there's no (easy) way to ensure that the array is exhaustive. I propose a new built-in type, ExhaustiveArray<SomeUnion>
, to do that.
🔍 Search Terms
tuple, complete list, all keys, all union members, array
✅ Viability Checklist
My suggestion meets these guidelines:
- This wouldn't be a breaking change in existing TypeScript/JavaScript code (all GH search results for
ExhaustiveArray
are in Java)This wouldn't change the runtime behavior of existing JavaScript codeThis could be implemented without emitting different JS based on the types of the expressionsThis isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)This feature would agree with the rest of TypeScript's Design Goals.
⭐ Suggestion
I'd like a way to check that an array contains all the keys on an interface. Right now I can do this:
interface MyInterface {
foo: string;
bar: string;
}
const keys: (keyof MyInterface)[] = ['foo', 'bar'];
...which will tell me if I've mistyped a key, but it won't tell me if a key is missing. So it's not future-proof if I add a new key to MyInterface
. It'd be nice if I could do something like this instead:
const keys: ExhaustiveArray<keyof MyInterface> = ['foo'];
// Error: Element 'bar' is missing in type '['foo']' but required in type 'ExhaustiveArray<keyof MyInterface>'.
// And it pairs well with the `satisfies` operator:
const keys: (keyof MyInterface)[] = ['foo'] satisfies ExhaustiveArray<keyof MyInterface>;
The order of elements doesn't matter, but type-checking fails if the array doesn't contain exactly 1 of every union member. This is a benefit over ToTuple<SomeUnion>
or something that depends on TypeScript's internal order of union members.
Generally, for a union X
with 2 members, ExhaustiveArray<X>
behaves the same as tuple type [X, X]
:
type Union = 'foo' | 'bar';
type Arr = ExhaustiveArray<Union>;
type x = Arr['length']; // 2
type y = Arr[0]; // Union
type z = Arr[5]; // Error: Tuple type 'Arr' of length '2' has no element at index '5'.
It is an error to pass a union that contains a non-literal type:
type x = ExhaustiveArray<string>; // Error: An ExhaustiveArray can only take a union of literal types.
type y = ExhaustiveArray<'foo'>; // compiles
I'm not sure how I'd handle optional keys, but given keyof
doesn't preserve that info anyway, it's probably moot. Maybe something like this?
const allKeys: AllKeys[] = [
...['foo'] satisfies ExhaustiveArray<RequiredKeys>,
...['bar'] satisfies OptionalKeys[]
];
📃 Motivating Example
💻 Use Cases
There is an existing way to do this (see stackoverflow answer), but the proposed solution is roundabout and affects the compiled code (introduces an unnecessary curried function call).
Other solutions might attempt to turn the union into a tuple. This depends on TypeScript's internal union member order, and these functions tend to be recursive which means they may fail for large unions.
The use case that motivated this was an attempt to iterate over a subset of keys in an object, to clear all errors from a state object:
interface Errors {
error_foo: string;
error_bar: string;
}
interface State extends Errors {
foo: string;
}
const errorKeys: (keyof Errors)[] = ['error_foo']; // compiles even though `error_bar` is missing
function clearErrors(oldState: State): State {
const newState = { ...oldState };
for (const errorKey of errorKeys) newState[errorKey] = null;
return newState;
}
Activity
RyanCavanaugh commentedon Mar 9, 2023
You can do this today with no runtime impact:
mrjacobbloom commentedon Mar 10, 2023
Oh cool! That's a little less ergonomic than what I was thinking, but that's fine given this is a less common use case. Thank you for the quick response!
RyanCavanaugh commentedon Mar 10, 2023
It's possible there's a more clever solution to do it in 1 line; maybe hit up the folks in the Discord if they're looking for a challenge.
jakebailey commentedon Mar 12, 2023
I was nerd sniped by a related issue and found these:
yasammez commentedon Aug 9, 2023
Since I too stumbled down this particular rabbit hole, other readers might be interested in this library: