Imagine I have this array, which is the "source of truth" for my action names:
const ACTION_NAMES = ["ACTION_1","ACTION_1","ACTION_3"] as const;
// I'M USING "as const" SO I CAN GET A UNION TYPE OUT OF IT
type ACTION_NAMES = typeof ACTION_NAMES[number];
// THIS IS "ACTION_1" | "ACTION_2" | "ACTION_3"
Now I want an interface/type which should have all these properties (exhaustive), as in:
type SOME_TYPE = {
[key: string]: unknown
}
interface ACTION_PAYLOADS { // COULD ALSO USE type alias HERE
ACTION_1: {propA: string, propB: number},
ACTION_2: {propC: boolean},
ACTION_3: {propD: string}
}
Is there any type I can extend
the ACTION_PAYLOADS
interface from, so that Typescript will make sure that all ACTION_1 2 and 3
properties will be present, and all of their values will extend
the SOME_TYPE
type?
The idea is that Typescript ideally should complain if I'm missing one of the ACTION_
properties, and also if one of the values isn't of type SOME_TYPE
. Also it wouldn't allow me to add any extra properties, for example ACTION_4
.
It feels like I'm type checking a type. But can't I do it with some mapped type? Is there a way to achieve this?
You could use some helper types to trigger a compiler error if your interface doesn't conform to the desired constraints. It looks like this:
type Extends<T extends U, U> = void;
type MutuallyExtends<T extends U, U extends V, V = T> = void;
type CheckKeysOfActionPayloads =
MutuallyExtends<keyof ACTION_PAYLOADS, ACTION_NAMES>; // ok
type CheckValuesOfActionPayloads =
Extends<ACTION_PAYLOADS[ACTION_NAMES], SOME_TYPE>; // ok
Here, CheckKeysOfActionPayloads
will only compile if the keys of ACTION_PAYLOADS
and the type ACTION_NAMES
mutually extend each other... for a union of literals this means they need to be identical.
And CheckValuesOfActionPayload
will only compiler if the values of ACTION_PAYLOADS
(at the ACTION_NAMES
keys) are assignable to SOME_TYPE
.
For concreteness I'm going to change SOME_TYPE
to be something that can fail, and also let's make sure that ACTION_NAMES
has "ACTION_2"
in it (as opposed to your example code):
const ACTION_NAMES = ["ACTION_1", "ACTION_2", "ACTION_3"] as const;
type ACTION_NAMES = typeof ACTION_NAMES[number];
type SOME_TYPE = {
[key: string]: string | number | boolean
}
Let's see what happens if we mess up ACTION_PAYLOADS
. First, if we miss a key:
interface ACTION_PAYLOADS {
ACTION_1: { propA: string, propB: number },
// where's ACTION_2?
ACTION_3: { propD: string }
}
type CheckKeysOfActionPayloads =
MutuallyExtends<keyof ACTION_PAYLOADS, ACTION_NAMES>; // error!
// ----------------------------------> ~~~~~~~~~~~~
// Type "ACTION_2" is not assignable to type "ACTION_1" | "ACTION_3"
Or if we have an extra key:
interface ACTION_PAYLOADS {
ACTION_1: { propA: string, propB: number },
ACTION_2: { propC: boolean },
ACTION_3: { propD: string },
ACTION_4: { propE: number } // what's this?
}
type CheckKeysOfActionPayloads =
MutuallyExtends<keyof ACTION_PAYLOADS, ACTION_NAMES>; // error!
// ---------------> ~~~~~~~~~~~~~~~~~~~~~
// Type "ACTION_4" is not assignable to type "ACTION_1" | "ACTION_2" | "ACTION_3"
And finally if the keys are right but a value is wrong:
interface ACTION_PAYLOADS {
ACTION_1: { propA: string, propB: number },
ACTION_2: { propC: Date }, // what's this?
ACTION_3: { propD: string }
}
type CheckValuesOfActionPayloads =
Extends<ACTION_PAYLOADS[ACTION_NAMES], SOME_TYPE>; // error!
// -------> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Type 'Date' is not assignable to type 'string | number | boolean'
You can see that in all those cases you get an error that can hopefully be used to help you fix the code.