Home > Mobile >  Enforce that subtype of type matches certain criteria
Enforce that subtype of type matches certain criteria

Time:10-11

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.

playground link

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).

  • Related