Skip to content

Instantly share code, notes, and snippets.

@jmakeig
Last active December 7, 2025 23:25
Show Gist options
  • Select an option

  • Save jmakeig/c009df612f526bd10d4da757377b9734 to your computer and use it in GitHub Desktop.

Select an option

Save jmakeig/c009df612f526bd10d4da757377b9734 to your computer and use it in GitHub Desktop.

Given a type Thing create a type Pending<Thing> with the following behavior:

  • Root-level properties of type ID are nullable
  • Properites that are entities, i.e. that have top-level ID property, are replaced with { id_prop: ID; }, where id_prop is name name of the entity’s ID property.
  • Same for Arrays of entities with ID properties, i.e. Array<{something: ID; …}> is mapped to Array<Ref<Entity>>
  • Primitives are loosened to allow the original type along with string or null, for example for easy instantiation from FormData
/*** Entities ***/
type Exercise = {
exercise: ID;
name: string;
description: string | null;
alternatives: Array<Exercise>;
warmdown: Exercise | null;
};
type WorkoutSet = Array<
{ exercise: Exercise } & { duration: number }
>;
type Workout = {
workout: ID;
name: string;
description: string | null;
sets: Array<WorkoutSet>;
};
/*
// Pending<Entity> implemented by hand
type PendingExercise = {
exercise?: never;
name: string | null;
description: string | null;
alternatives: Array<Ref<Exercise>>;
warmdown: Ref<Exercise> | null;
};
type PendingWorkout = {
workout?: never;
name: string | null;
description: string | null;
sets: Array<PendingWorkoutSet>
};
type PendingWorkoutSet = Array<
{ exercise: Ref<Exercise> } & { duration: number }
>
*/
const pws: Pending<WorkoutSet> = [
{ exercise: { exercise: '1234' as ID }, duration: 1000 },
{ exercise: { exercise: '5678' as ID }, duration: 250 }
];
const e1: Exercise = {
exercise: 'asdf' as ID,
name: 'Asdf',
description: null,
alternatives: [
{
exercise: 'asdf' as ID,
name: 'Asdf',
description: null,
alternatives: [],
warmdown: null
}
],
warmdown: null
};
const pe2: Pending<Exercise> = {
exercise: null,
name: 'Asdf',
description: null,
alternatives: [
{
exercise: 'asdf' as ID
},
null
],
warmdown: { exercise: 'asdf' as ID }
}
const pe4: Pending<Exercise> = {
exercise: null,
name: 'Asdf',
description: null,
alternatives: [
{
exercise: 'asdf' as ID
},
null
],
warmdown: null
}
const pe3_nope: Pending<Exercise> = {
exercise: 'asdf' as ID,
name: 'Asdf',
description: null,
alternatives: [
{
exercise: 'asdf' as ID,
// @ts-expect-error
name: 'Asdf',
description: null,
alternatives: [],
warmdown: null
}
],
warmdown: null
};
const ws: WorkoutSet = [
{
exercise: {
exercise: 'asdf' as ID,
name: 'Asdf',
description: null,
alternatives: [
{
exercise: 'asdf' as ID,
name: 'Asdf',
description: null,
alternatives: [],
warmdown: null
}
],
warmdown: null
}, duration: 1000
},
{
exercise: {
exercise: 'asdf' as ID,
name: 'Asdf',
description: null,
alternatives: [
{
exercise: 'asdf' as ID,
name: 'Asdf',
description: null,
alternatives: [],
warmdown: null
}
],
warmdown: null
}, duration: 1000
}
];
const w: Workout = {
workout: 'asdfasdf' as ID,
name: '15-minute Cardio',
description: null,
sets: [
ws, ws
]
};
const pw: Pending<Workout> = {
workout: 'workout' as ID,
name: null,
description: null,
sets: [
[
{ exercise: { exercise: '1234' as ID }, duration: 1000 },
{ exercise: { exercise: '5678' as ID }, duration: 250 }
],
[
{ exercise: { exercise: '1234' as ID }, duration: '1000' },
{ exercise: { exercise: '5678' as ID }, duration: '250' }
]
]
};
/*** Utilities ***/
declare const IDBrand: unique symbol;
export type ID = string & { [IDBrand]: void };
/**
* Uses the property in `Entity` that is of type `ID`.
*/
type Ref<Entity> = {
[P in IDPropertyKey<Entity>]: ID; // Use entity’s ID key name, not general `ref`
};
/**
* The `string` key name of the property of `Entity` that is *exactly* of type `ID`.
*/
type IDPropertyKey<Entity> = {
[K in keyof Entity]: Entity[K] extends ID
? ([ID] extends [Entity[K]] ? K : never) // Clever exact match trick
: never
}[keyof Entity];
/**
* Union type for all non-object types. Does not include `Array`.
*/
type Primitive = string | number | bigint | boolean | symbol | null | undefined;
/**
* `boolean` check that `T` is not `ID`, but is a `Primitive`
*/
type IsPrimitive<T> = T extends ID ? false : T extends Primitive ? true : false;
/**
* `boolean` check that `T` is an `Array`
*/
type IsArray<T> = T extends Array<any> ? true : false;
/**
* An `Entity` is not a `Primitive` or and `Array` and it has a property of type `ID` (`IDPropertyKey<Entity>`)
*/
type IsEntity<Entity> =
IsPrimitive<Entity> extends true ? false
: IsArray<Entity> extends true ? false
: IDPropertyKey<Entity> extends never ? false
: true;
/**
* Applies the `Ref` mapping logic to each element of an `Array`
*/
type TransformArray<T> = T extends Array<infer U>
? Array<
U extends object
? IsEntity<U> extends true
? Ref<U> | null
: Pending<U>
: U
>
: T;
/**
* Special casing for tranforming `Entity` types that aren’t nullable
*/
type TransformNonNullable<Entity> = IsEntity<Entity> extends true
? Ref<Entity>
: Pending<Entity>;
/**
* Special handling to retain nullabiltiy on `Ref`s
*/
type TransformForeignRef<T> = T extends null //| undefined
? TransformNonNullable<NonNullable<T>> | null // Transform non-nullable part, then re-apply null | undefined
: TransformNonNullable<T>; // Transform directly if not nullable
/**
* Loosens an `Entity` type to allow:
* * References to other entities, rather than having to fully instantiate them
* * Nullable `ID` keys, for example, for the case of a new instance that doesn’t have an identifier yet
* * Primitive values can use the strong type as well as `string` or `null`, for example when mapping from `FormData`
*/
export type Pending<Entity> = Entity extends Array<any> ? TransformArray<Entity> : {
[K in keyof Entity]
// Rule 1: Primary ID property
: K extends IDPropertyKey<Entity>
? ID | null
// Rule 4: Array properties (must be second)
: Entity[K] extends Array<any>
? TransformArray<Entity[K]>
// Rule 2: Primitive handling
: Entity[K] extends Primitive // Numbers become number | null
? Entity[K] | string | null
// Rule 3 (and final fall-through): Entity/Object reference transformation
: TransformForeignRef<Entity[K]>
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment