Skip to content

Instantly share code, notes, and snippets.

@ehvattum
Last active August 9, 2023 12:48
Show Gist options
  • Save ehvattum/7d68a63be96911580ad7d06dfb946abb to your computer and use it in GitHub Desktop.
Save ehvattum/7d68a63be96911580ad7d06dfb946abb to your computer and use it in GitHub Desktop.
Create a JSONPatch-ish type for any object
import { Primitive, Simplify, WritableKeysOf } from "type-fest"
/**
* Create a JSON-patch dto type for a given type.
* * Strips out readonly properties (WritableKeysOf)
*/
export type Patch<T> = PatchRoot<Pick<T, WritableKeysOf<T>>>
/** Make patch schema for all useful keys in type */
type PatchRoot<TRoot extends object> = {
[Key in keyof TRoot]: Key extends string ? Simplify<Operations<Key, TRoot[Key]>> : never
}[keyof TRoot][]
/** Define operations for a property */
type Operations<TKey extends string, TValue> = [TValue] extends [(infer Item)[]]
? ArrayOperations<TKey, Item>
: TValue extends Simple
? SimpleOperation<TKey, TValue>
: TValue extends object
? ObjectOperations<TKey, TValue>
: never
/** Define operations for nested properties */
type ObjectOperations<OwnName extends string, TObject extends object> = {
[Key in keyof TObject]: Key extends string
? Operations<`${OwnName}.${Key}`, TObject[Key]>
: never
}[keyof TObject]
/** Define operations for properties on objects in an Array */
type Recurse<Key extends string, ItemType> = Operations<`${Key}[${number}]`, ItemType>
/** Define operations for array properties and for operations on items in the array */
type ArrayOperations<TKey extends string, TItem> =
| RemoveOp<TKey>
| ReplaceOp<TKey, TItem[]>
| PushOp<TKey, TItem>
| InjectAtOp<TKey, TItem>
| SetAtOp<TKey, TItem>
| RemoveAtOp<TKey>
| Recurse<TKey, TItem> // Add in object-operations for items in the array
/** Basic value types */
type Simple = Primitive
/**
* Creates a type that describes the patch operation for a simple type
*/
type SimpleOperation<Key extends string, Value extends Simple> =
| ReplaceOp<Key, Value>
| (Value extends undefined ? RemoveOp<Key> : never)
/** Removes the entire property */
export type RemoveOp<Key extends string> = {
path: Key
op: "remove"
}
/** Replaces the entire property */
export type ReplaceOp<Key extends string, Value> = Value extends undefined
? never
: {
path: Key
op: "replace"
value: Value
}
/** Adds an item to the end of the array */
export type PushOp<Key extends string, ItemType> = {
path: Key
op: "add"
value: ItemType
}
/** Injects item at index, moves existing items out of the way. */
export type InjectAtOp<Key extends string, ItemType> = {
path: `${Key}[${number}]`
op: "add"
value: ItemType
}
/** Replaces the item at index. */
export type SetAtOp<Key extends string, ItemType> = {
path: `${Key}[${number}]`
op: "replace"
value: ItemType
}
/** Removes the item at index. */
export type RemoveAtOp<Key extends string> = {
path: `.${Key}[${number}]`
op: "remove"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment