UNPKG

@temporalio/common

Version:

Common library for code that's used across the Client, Worker, and/or Workflow

171 lines (170 loc) 10.2 kB
import { Exact, RemovePrefix, UnionToIntersection } from '../type-helpers'; /** * Create encoding and decoding functions to convert between the numeric `enum` types produced by our * Protobuf compiler and "const object of strings" enum values that we expose in our public APIs. * * ### Usage * * Newly introduced enums should follow the following pattern: * * ```ts * type ParentClosePolicy = (typeof ParentClosePolicy)[keyof typeof ParentClosePolicy]; * const ParentClosePolicy = { * TERMINATE: 'TERMINATE', * ABANDON: 'ABANDON', * REQUEST_CANCEL: 'REQUEST_CANCEL', * } as const; * * const [encodeParentClosePolicy, decodeParentClosePolicy] = // * makeProtoEnumConverters< * coresdk.child_workflow.ParentClosePolicy, * typeof coresdk.child_workflow.ParentClosePolicy, * keyof typeof coresdk.child_workflow.ParentClosePolicy, * typeof ParentClosePolicy, * 'PARENT_CLOSE_POLICY_' // This may be an empty string if the proto enum doesn't add a repeated prefix on values * >( * { * [ParentClosePolicy.TERMINATE]: 1, // These numbers must match the ones in the proto enum * [ParentClosePolicy.ABANDON]: 2, * [ParentClosePolicy.REQUEST_CANCEL]: 3, * * UNSPECIFIED: 0, * } as const, * 'PARENT_CLOSE_POLICY_' * ); * ``` * * `makeProtoEnumConverters` supports other usage patterns, but they are only meant for * backward compatibility with former enum definitions and should not be used for new enums. * * ### Context * * Temporal's Protobuf APIs define several `enum` types; our Protobuf compiler transforms these to * traditional (i.e. non-const) [TypeScript numeric `enum`s](https://www.typescriptlang.org/docs/handbook/enums.html#numeric-enums). * * For various reasons, this is far from ideal: * * - Due to the dual nature of non-const TypeScript `enum`s (they are both a type and a value), * it is not possible to refer to an enum value from code without a "real" import of the enum type * (i.e. can't simply do `import type ...`). In Workflow code, such an import would result in * loading our entire Protobuf definitions into the workflow sandbox, adding several megabytes to * the per-workflow memory footprint, which is unacceptable; to avoid that, we need to maintain * a mirror copy of each enum types used by in-workflow APIs, and export these from either * `@temporalio/common` or `@temporalio/workflow`. * - It is not desirable for users to need an explicit dependency on `@temporalio/proto` just to * get access to these enum types; we therefore made it a common practice to reexport these enums * from our public facing packages. However, experience demontrated that these reexports effectively * resulted in poor and inconsistent documentation coverage compared to mirrored enums types. * - Our Protobuf enum types tend to follow a verbose and redundant naming convention, which feels * unatural and excessive according to most TypeScript style guides; e.g. instead of * `workflowIdReusePolicy: WorkflowIdReusePolicy.WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE`, * a TypeScript developer would generally expect to be able to write something similar to * `workflowIdReusePolicy: 'REJECT_DUPLICATE'`. * - Because of the way Protobuf works, many of our enum types contain an `UNSPECIFIED` value, which * is used to explicitly identify a value that is unset. In TypeScript code, the `undefined` value * already serves that purpose, and is definitely more idiomatic to TS developers, whereas these * `UNSPECIFIED` values create noise and confusion in our APIs. * - TypeScript editors generally do a very bad job at providing autocompletion that implies reaching * for values of a TypeScript enum type, forcing developers to explicitly type in at least part * of the name of the enum type before they can get autocompletion for its values. On the other * hand, all TS editors immediately provide autocompletion for string union types. * - The [TypeScript's official documentation](https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums) * itself suggests that, in modern TypeScript, the use of `as const` objects may generally suffice * and may be advantageous over the use of `enum` types. * * A const object of strings, combined with a union type of possible string values, provides a much * more idiomatic syntax and a better DX for TypeScript developers. This however requires a way to * convert back and forth between the `enum` values produced by the Protobuf compiler and the * equivalent string values. * * This helper dynamically creates these conversion functions for a given Protobuf enum type, * strongly building upon specific conventions that we have adopted in our Protobuf definitions. * * ### Validations * * The complex type signature of this helper is there to prevent most potential incoherencies * that could result from having to manually synchronize the const object of strings enum and the * conversion table with the proto enum, while not requiring a regular import on the Protobuf enum * itself (so it can be used safely for enums meant to be used from workflow code). * * In particular, failing any of the following invariants will result in build time errors: * * - For every key of the form `PREFIX_KEY: number` in the proto enum, excluding the `UNSPECIFIED` key: * - There MUST be a corresponding `KEY: 'KEY'` entry in the const object of strings enum; * - There MAY be a corresponding `PREFIX_KEY: 'KEY'` in the const object of strings enum * (this is meant to preserve backward compatibility with the former syntax; such aliases should * not be added for new enums and enum entries introduced going forward); * - There MUST be a corresponding `KEY: number` in the mapping table. * - If the proto enum contains a `PREFIX_UNSPECIFIED` entry, then: * - There MAY be a corresponding `PREFIX_UNSPECIFIED: undefined` and/or `UNSPECIFIED: undefined` * entries in the const object of strings enum — this is meant to preserve backward compatibility * with the former syntax; this alias should not be added for new enums introduced going forward; * - There MUST be an `UNSPECIFIED: 0` in the mapping table. * - The const object of strings enum MUST NOT contain any other keys than the ones mandated or * optionally allowed be the preceeding rules. * - The mapping table MUST NOT contain any other keys than the ones mandated above. * * These rules notably ensure that whenever a new value is added to an existing Proto enum, the code * will fail to compile until the corresponding entry is added on the const object of strings enum * and the mapping table. * * @internal */ export declare function makeProtoEnumConverters<ProtoEnumValue extends number, ProtoEnum extends { [k in ProtoEnumKey]: ProtoEnumValue; }, ProtoEnumKey extends `${Prefix}${string}`, StringEnumTypeActual extends Exact<StringEnumType, StringEnumTypeActual>, Prefix extends string, Unspecified = ProtoEnumKey extends `${Prefix}UNSPECIFIED` ? 'UNSPECIFIED' : never, ShortStringEnumKey extends RemovePrefix<Prefix, ProtoEnumKey> = Exclude<RemovePrefix<Prefix, ProtoEnumKey>, Unspecified>, StringEnumType extends ProtoConstObjectOfStringsEnum<ShortStringEnumKey, Prefix, Unspecified> = ProtoConstObjectOfStringsEnum<ShortStringEnumKey, Prefix, Unspecified>, MapTable extends ProtoEnumToConstObjectOfStringMapTable<StringEnumType, ProtoEnumValue, ProtoEnum, ProtoEnumKey, Prefix, Unspecified, ShortStringEnumKey> = ProtoEnumToConstObjectOfStringMapTable<StringEnumType, ProtoEnumValue, ProtoEnum, ProtoEnumKey, Prefix, Unspecified, ShortStringEnumKey>>(mapTable: MapTable, prefix: Prefix): [ (input: ShortStringEnumKey | `${Prefix}${ShortStringEnumKey}` | ProtoEnumValue | null | undefined) => ProtoEnumValue | undefined, (input: ProtoEnumValue | null | undefined) => ShortStringEnumKey | undefined ]; /** * Given the exploded parameters of a proto enum (i.e. short keys, prefix, and short key of the * unspecified value), make a type that _exactly_ corresponds to the const object of strings enum, * e.g. the type that the developer is expected to write. * * For example, for coresdk.child_workflow.ParentClosePolicy, this evaluates to: * * { * TERMINATE: "TERMINATE"; * ABANDON: "ABANDON"; * REQUEST_CANCEL: "REQUEST_CANCEL"; * * PARENT_CLOSE_POLICY_TERMINATE?: "TERMINATE"; * PARENT_CLOSE_POLICY_ABANDON?: "ABANDON"; * PARENT_CLOSE_POLICY_REQUEST_CANCEL?: "REQUEST_CANCEL"; * * PARENT_CLOSE_POLICY_UNSPECIFIED?: undefined; * } */ type ProtoConstObjectOfStringsEnum<ShortStringEnumKey extends string, Prefix extends string, Unspecified> = UnionToIntersection<{ readonly [k in ShortStringEnumKey]: k; } | { [k in ShortStringEnumKey]: Prefix extends '' ? object : { readonly [kk in `${Prefix}${k}`]?: k; }; }[ShortStringEnumKey] | (Unspecified extends string ? { [k in `${Prefix}${Unspecified}`]?: undefined; } : object) | (Unspecified extends string ? { [k in `${Unspecified}`]?: undefined; } : object)>; /** * Given the exploded parameters of a proto enum (i.e. short keys, prefix, and short key of the * unspecified value), make a type that _exactly_ corresponds to the mapping table that the user is * expected to provide. * * For example, for coresdk.child_workflow.ParentClosePolicy, this evaluates to: * * { * UNSPECIFIED: 0, * TERMINATE: 1, * ABANDON: 2, * REQUEST_CANCEL: 3, * } */ type ProtoEnumToConstObjectOfStringMapTable<_StringEnum extends ProtoConstObjectOfStringsEnum<ShortStringEnumKey, Prefix, Unspecified>, ProtoEnumValue extends number, ProtoEnum extends { [k in ProtoEnumKey]: ProtoEnumValue; }, ProtoEnumKey extends `${Prefix}${string}`, Prefix extends string, Unspecified, ShortStringEnumKey extends RemovePrefix<Prefix, ProtoEnumKey>> = UnionToIntersection<{ [k in ProtoEnumKey]: { [kk in RemovePrefix<Prefix, k>]: ProtoEnum[k] extends number ? ProtoEnum[k] : never; }; }[ProtoEnumKey]>; export {};