UNPKG

@modernice/typed-response

Version:

Automatic types for JSON APIs

145 lines (143 loc) 5.36 kB
/** * `ResponseOf<T>` is the type `T`, normalized to primitive types that are * supported by JSON. * * If `T` is a primitive type, then `ResponseOf<T> == T`. * If `T` is an object, then each property of `T` is recursively normalized to * a primitive type. * * ## Examples * * ### Default mapping * * By default, `ResponseOf<T>` recursively replaces {@link Date} with `string` * and does nothing more. * * ```ts * type Foo = { * a: string * b: number * c: boolean * d: Date * e: { * g: { * h: Date * } * i: [string, number, Date] * } * * ResponseOf<Foo> == { * a: string * b: number * c: boolean * d: string // Dates are normalized to strings by default * e: { * g: { * h: string // types are mapped recursively * } * i: [string, number, string] // Arrays and tuples are also mapped * } * ``` * * ### Custom mapping * * You can provide a custom mapping for each field of `T` as the second generic * type. When provided, the mapping overrides the default mapping for type `T`. * * ```ts * type Person = { * name: string * age: string * birthdate: Date * contact: { * email: string * lastContacted: Date * } * } * * type Mapping = { * age: number * birthdate: number * contact: { * lastContacted: { // if for example an API returns dates as objects with a `timestamp` property * timestamp: number * } * } * } * * ResponseOf<Person, Mapping> == { * name: string * age: number * birthdate: number * contact: { * email: string * lastContacted: { * timestamp: number * } * } * } * ``` * * Given the example above, we can write a strongly-typed `hydratePerson` * function that converts a JSON response to a `Person`: * * ```ts * function hydratePerson(data: ResponseOf<Person, Mapping>): Person { * return { * ...data, * age: `${data.age} years` * birthdate: new Date(data.birthdate), * contact: { * ...data.contact, * lastContacted: new Date(data.contact.lastContacted.timestamp), * }, * } * } * ``` */ declare type ResponseOf<T, CustomMapping extends Mapping<T> = never> = [ CustomMapping ] extends [never] ? ReplacePrimitives<T, CustomMapping> : ApplyMapping<CustomMapping, ReplacePrimitives<T, CustomMapping>>; declare type ReplacePrimitives<T, TNotNull = Exclude<T, null>, TNotUndefined = Exclude<T, undefined>, TStrict = Exclude<T, null | undefined>> = T extends null ? ReplacePrimitives<TNotNull> | null : T extends undefined ? ReplacePrimitives<TNotUndefined> | undefined : TStrict extends string | number | boolean ? TStrict : TStrict extends Date ? string : [T] extends [Mappable] ? { [K in keyof T]: ReplacePrimitives<T[Exclude<K, undefined>]>; } : unknown; /** * Mappings of the properties of `T` to custom types. */ declare type Mapping<T> = { [key in keyof T]?: any; }; /** * Applies the given {@link Mapping} to `T`. `T` must be an object of type * `Record<string, unknown>`. */ declare type ApplyMapping<M extends Mapping<T>, T> = T extends Record<string, unknown> ? Expand<ApplyOptionalMapping<M, T> & ApplyRequiredMapping<M, T>> : T; declare type ApplyOptionalMapping<M extends Mapping<T>, T extends Mappable> = { [K in keyof PickOptionalProperties<T> as undefined extends ApplyMappingToProperty<M, T, string & K> ? K : never]?: ApplyMappingToProperty<M, T, string & K>; } & { [K in keyof PickOptionalProperties<T> as undefined extends ApplyMappingToProperty<M, T, string & K> ? never : K]: ApplyMappingToProperty<M, T, string & K>; }; declare type ApplyRequiredMapping<M extends Mapping<T>, T extends Mappable> = { [K in keyof PickRequiredProperties<T> as undefined extends ApplyMappingToProperty<M, T, string & K> ? never : K]: ApplyMappingToProperty<M, T, string & K>; }; declare type ApplyMappingToProperty<Map extends Mapping<Obj>, Obj extends Mappable, Prop extends keyof Obj, ValueIsMappable = Obj[Prop] extends Mappable ? true : false, ValueIsArray = Obj[Prop] extends Array<unknown> ? true : false, SubMapping = true extends ValueIsMappable ? unknown extends Map[Prop] ? {} : Map[Prop] : never, AppliedSubMapping = ApplyMapping<SubMapping, Obj[Prop]>> = unknown extends Map[Prop] ? Obj[Prop] : false extends ValueIsMappable ? Map[Prop] : true extends ValueIsArray ? Map[Prop] : AppliedSubMapping; declare type Mappable = Record<string, any>; declare type OptionalKeys<T> = { [K in keyof T]-?: {} extends Pick<T, K> ? K : never; }[keyof T]; declare type RequiredKeys<T> = Exclude<keyof T, OptionalKeys<T>>; declare type Primitive = string | number | boolean | undefined | null; declare type PickOptionalProperties<T> = T extends Primitive ? T : T extends Array<infer U> ? PickOptionalPropertiesArray<U> : PickOptionalPropertiesObject<T>; interface PickOptionalPropertiesArray<T> extends ReadonlyArray<PickOptionalProperties<T>> { } declare type PickOptionalPropertiesObject<T> = { [P in OptionalKeys<T>]: T[P]; }; declare type PickRequiredProperties<T> = T extends Record<string, unknown> ? PickRequiredPropertiesObject<T> : T; declare type PickRequiredPropertiesObject<T> = { [P in RequiredKeys<T>]: T[P]; }; declare type Expand<T> = T extends Record<string, unknown> ? { [K in keyof T]: Expand<T[K]>; } : T; export { ApplyMapping, Mapping, ResponseOf };