diff --git a/crates/bindings-typescript/src/lib/type_util.test-d.ts b/crates/bindings-typescript/src/lib/type_util.test-d.ts new file mode 100644 index 00000000000..80824b999d3 --- /dev/null +++ b/crates/bindings-typescript/src/lib/type_util.test-d.ts @@ -0,0 +1,56 @@ +import type { ConnectionId } from './connection_id'; +import type { Identity } from './identity'; +import type { Prettify } from './type_util'; +import type { TimeDuration } from './time_duration'; +import type { Timestamp } from './timestamp'; +import type { Uuid } from './uuid'; + +declare const brand: unique symbol; + +/** + * A branded primitive: an intersection of a primitive with a marker object. + * `Prettify` must leave it alone — mapping it produces a structural record of + * `String`'s members that no longer satisfies `string`. + */ +type UserId = string & { readonly [brand]: 'UserId' }; + +declare const userId: Prettify; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const _brandedIsStillAString: string = userId; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const _brandedKeepsItsBrand: UserId = userId; + +// Plain primitives round-trip unchanged. +declare const str: Prettify; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const _str: string = str; +declare const num: Prettify; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const _num: number = num; +declare const big: Prettify; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const _big: bigint = big; + +// Every SATS wrapper class is passed through as the class, not as a +// structural record of its members. +declare const identity: Prettify; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const _identity: Identity = identity; +declare const connectionId: Prettify; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const _connectionId: ConnectionId = connectionId; +declare const timestamp: Prettify; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const _timestamp: Timestamp = timestamp; +declare const timeDuration: Prettify; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const _timeDuration: TimeDuration = timeDuration; +declare const uuid: Prettify; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const _uuid: Uuid = uuid; + +// Object types are still flattened. +type Intersected = { a: string } & { b: number }; +declare const flattened: Prettify; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const _flattened: { a: string; b: number } = flattened; diff --git a/crates/bindings-typescript/src/lib/type_util.ts b/crates/bindings-typescript/src/lib/type_util.ts index 942b76a1424..789595b24e8 100644 --- a/crates/bindings-typescript/src/lib/type_util.ts +++ b/crates/bindings-typescript/src/lib/type_util.ts @@ -3,20 +3,28 @@ import type { Identity } from './identity'; import type { ScheduleAt } from './schedule_at'; import type { TimeDuration } from './time_duration'; import type { Timestamp } from './timestamp'; +import type { Uuid } from './uuid'; type DoNotPrettify = | Identity | ConnectionId | Timestamp | TimeDuration - | ScheduleAt; + | ScheduleAt + | Uuid; /** * Utility to make TS show cleaner types by flattening intersections. + * + * Only object types are flattened. A non-object has no intersection to + * flatten, and mapping one is not always identity: `string & Brand` becomes a + * structural record of `String`'s members that no longer satisfies `string`. */ export type Prettify = T extends DoNotPrettify ? T - : { [K in keyof T]: T[K] } & {}; + : T extends string | number | boolean | bigint | symbol | null | undefined + ? T + : { [K in keyof T]: T[K] } & {}; /** * Helper function to sets a field in an object