Background: I'm using the JSONObject json-serializable type definition from https://stackoverflow.com/a/64117261/5602521, and the method of mapping enums to types from https://stackoverflow.com/a/72705814/5602521, so I have the following:
type JSONValue =
| string
| number
| boolean
| null
| JSONValue[]
| { [key: string]: JSONValue };
export interface JSONObject {
[k: string]: JSONValue;
}
export enum ClientEvent {
PAGE_VIEW = 'page-view',
FOO_BUTTON_CLICKED = 'foo-button-clicked',
}
export type ClientEventArgs = {
[Key in ClientEvent]: {
[ClientEvent.PAGE_VIEW]: {
url: string;
};
[ClientEvent.FOO_BUTTON_CLICKED]: {
fooValue: number;
};
}[Key];
};
export function logClientEvent<EventType extends ClientEvent>(
eventType: EventType,
data: ClientEventArgs[EventType],
): void {}
My goal is to use the type system to enforce the following:
a) Every enum value in ClientEvent
must have a corresponding key/value pair in ClientEventArgs
.
b) The type of the value of each key/value pair in ClientEventArgs
is compatible with the JSONObject
type.
c) When calling logClientEvent
with a given enum value as the first argument, the type of the second argument must match the type defined in ClientEventArgs
for that enum value.
My above code achieves a) and c), but not b).
Does anyone know how to enforce that the inner types in ClientEventArgs
match the JSONObject
type?
(And sorry for the horrible question title by the way; I'm not even sure how to describe what I'm trying to do here, otherwise I could probably just google it)
EDIT: The following almost works, except that it compiles just fine until you try to call logClientEvent
for the offending enum value.
type ClientEventArgsInner = {
[ClientEvent.PAGE_VIEW]: {
url: string;
};
[ClientEvent.FOO_BUTTON_CLICKED]: {
// This should not be allowed by the type system:
fooValue: () => void;
};
};
export type ClientEventArgs = {
[Key in ClientEvent]: ClientEventArgsInner[Key] extends JSONObject ? ClientEventArgsInner[Key] : never;
};
// ...
// Type system only complains about the 'never' when this is uncommented:
// logClientEvent(ClientEvent.FOO_BUTTON_CLICKED, {fooValue: () => {}});
CodePudding user response:
One way to go about it would be
type IndexedJSONObjects = {
[key: string]: JSONObject
}
type ValidateJSONValues<T extends IndexedJSONObjects> = T;
type ClientEventArgsInner = ValidateJSONValues<{
[ClientEvent.PAGE_VIEW]: {
url: string;
};
[ClientEvent.FOO_BUTTON_CLICKED]: {
// This should not be allowed by the type system:
fooValue: () => void;
};
}>;
or, to avoid the long error line,
type ClientEventArgsInner = {
[ClientEvent.PAGE_VIEW]: {
url: string;
};
[ClientEvent.FOO_BUTTON_CLICKED]: {
// This should not be allowed by the type system:
fooValue: () => void;
};
};
type ValidateClientEventArgsInner = ValidateJSONValues<ClientEventArgsInner>
none of which are as clean as one would ideally want it to be.
CodePudding user response:
One solution is to conditionally omit keys from ClientEventArgs
:
If you provide a non-matching type, this will cause the definition of logClientEvent
to throw this exception: Type 'EventType' cannot be used to index type 'ClientEventArgs'
:
This would look something like
export type ClientEventArgs = {
[Key in ClientEvent as ClientEventArgsInner[Key] extends JSONObject ? Key : never]: ClientEventArgsInner[Key];
};
Note that it's important to have this condition in the key rather than the value of the mapped type. According to https://github.com/microsoft/TypeScript/issues/23199 , returning never
in the value will result in the key being present with the type of never
.
For example, you might get a result like
{
banana: never;
alice: Apple;
gala: Apple;
}
What we want to do is return never
in the key, which will actually exclude the key, like
{
alice: Apple;
gala: Apple;
}
We want to exclude this key, because that will cause the aforementioned Type 'EventType' cannot be used to index type 'ClientEventArgs'
error for bad types (because if the key is excluded, ClientEventArgs
will be missing at least one key from ClientEvent
).