UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

916 lines (837 loc) 29.7 kB
import type { IStreamBuilder } from '@tanstack/db-ivm' import type { Collection } from './collection/index.js' import type { StandardSchemaV1 } from '@standard-schema/spec' import type { Transaction } from './transactions' import type { BasicExpression, OrderBy } from './query/ir.js' import type { EventEmitter } from './event-emitter.js' /** * Interface for a collection-like object that provides the necessary methods * for the change events system to work */ export interface CollectionLike< T extends object = Record<string, unknown>, TKey extends string | number = string | number, > extends Pick< Collection<T, TKey>, `get` | `has` | `entries` | `indexes` | `id` | `compareOptions` > {} /** * StringSortOpts - Options for string sorting behavior * * This discriminated union allows for two types of string sorting: * - **Lexical**: Simple character-by-character comparison (default) * - **Locale**: Locale-aware sorting with optional customization * * The union ensures that locale options are only available when locale sorting is selected. */ export type StringCollationConfig = | { stringSort?: `lexical` } | { stringSort?: `locale` locale?: string localeOptions?: object } /** * Helper type to extract the output type from a standard schema * * @internal This is used by the type resolution system */ export type InferSchemaOutput<T> = T extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<T> extends object ? StandardSchemaV1.InferOutput<T> : Record<string, unknown> : Record<string, unknown> /** * Helper type to extract the input type from a standard schema * * @internal This is used for collection insert type inference */ export type InferSchemaInput<T> = T extends StandardSchemaV1 ? StandardSchemaV1.InferInput<T> extends object ? StandardSchemaV1.InferInput<T> : Record<string, unknown> : Record<string, unknown> export type TransactionState = `pending` | `persisting` | `completed` | `failed` /** * Represents a utility function that can be attached to a collection */ export type Fn = (...args: Array<any>) => any /** * A record of utilities (functions or getters) that can be attached to a collection */ export type UtilsRecord = Record<string, any> /** * * @remarks `update` and `insert` are both represented as `Partial<T>`, but changes for `insert` could me made more precise by inferring the schema input type. In practice, this has almost 0 real world impact so it's not worth the added type complexity. * * @see https://github.com/TanStack/db/pull/209#issuecomment-3053001206 */ export type ResolveTransactionChanges< T extends object = Record<string, unknown>, TOperation extends OperationType = OperationType, > = TOperation extends `delete` ? T : Partial<T> /** * Represents a pending mutation within a transaction * Contains information about the original and modified data, as well as metadata */ export interface PendingMutation< T extends object = Record<string, unknown>, TOperation extends OperationType = OperationType, TCollection extends Collection<T, any, any, any, any> = Collection< T, any, any, any, any >, > { mutationId: string // The state of the object before the mutation. original: TOperation extends `insert` ? {} : T // The result state of the object after all mutations. modified: T // Only the actual changes to the object by the mutation. changes: ResolveTransactionChanges<T, TOperation> globalKey: string key: any type: TOperation metadata: unknown syncMetadata: Record<string, unknown> /** Whether this mutation should be applied optimistically (defaults to true) */ optimistic: boolean createdAt: Date updatedAt: Date collection: TCollection } /** * Configuration options for creating a new transaction */ export type MutationFnParams<T extends object = Record<string, unknown>> = { transaction: TransactionWithMutations<T> } export type MutationFn<T extends object = Record<string, unknown>> = ( params: MutationFnParams<T>, ) => Promise<any> /** * Represents a non-empty array (at least one element) */ export type NonEmptyArray<T> = [T, ...Array<T>] /** * Utility type for a Transaction with at least one mutation * This is used internally by the Transaction.commit method */ export type TransactionWithMutations< T extends object = Record<string, unknown>, TOperation extends OperationType = OperationType, > = Omit<Transaction<T>, `mutations`> & { /** * We must omit the `mutations` property from `Transaction<T>` before intersecting * because TypeScript intersects property types when the same property appears on * both sides of an intersection. * * Without `Omit`: * - `Transaction<T>` has `mutations: Array<PendingMutation<T>>` * - The intersection would create: `Array<PendingMutation<T>> & NonEmptyArray<PendingMutation<T, TOperation>>` * - When mapping over this array, TypeScript widens `TOperation` from the specific literal * (e.g., `"delete"`) to the union `OperationType` (`"insert" | "update" | "delete"`) * - This causes `PendingMutation<T, OperationType>` to evaluate the conditional type * `original: TOperation extends 'insert' ? {} : T` as `{} | T` instead of just `T` * * With `Omit`: * - We remove `mutations` from `Transaction<T>` first * - Then add back `mutations: NonEmptyArray<PendingMutation<T, TOperation>>` * - TypeScript can properly narrow `TOperation` to the specific literal type * - This ensures `mutation.original` is correctly typed as `T` (not `{} | T`) when mapping */ mutations: NonEmptyArray<PendingMutation<T, TOperation>> } export interface TransactionConfig<T extends object = Record<string, unknown>> { /** Unique identifier for the transaction */ id?: string /* If the transaction should autocommit after a mutate call or should commit be called explicitly */ autoCommit?: boolean mutationFn: MutationFn<T> /** Custom metadata to associate with the transaction */ metadata?: Record<string, unknown> } /** * Options for the createOptimisticAction helper */ export interface CreateOptimisticActionsOptions< TVars = unknown, T extends object = Record<string, unknown>, > extends Omit<TransactionConfig<T>, `mutationFn`> { /** Function to apply optimistic updates locally before the mutation completes */ onMutate: (vars: TVars) => void /** Function to execute the mutation on the server */ mutationFn: (vars: TVars, params: MutationFnParams<T>) => Promise<any> } export type { Transaction } type Value<TExtensions = never> = | string | number | boolean | bigint | null | TExtensions | Array<Value<TExtensions>> | { [key: string | number | symbol]: Value<TExtensions> } export type Row<TExtensions = never> = Record<string, Value<TExtensions>> export type OperationType = `insert` | `update` | `delete` /** * Subscription status values */ export type SubscriptionStatus = `ready` | `loadingSubset` /** * Event emitted when subscription status changes */ export interface SubscriptionStatusChangeEvent { type: `status:change` subscription: Subscription previousStatus: SubscriptionStatus status: SubscriptionStatus } /** * Event emitted when subscription status changes to a specific status */ export interface SubscriptionStatusEvent<T extends SubscriptionStatus> { type: `status:${T}` subscription: Subscription previousStatus: SubscriptionStatus status: T } /** * Event emitted when subscription is unsubscribed */ export interface SubscriptionUnsubscribedEvent { type: `unsubscribed` subscription: Subscription } /** * All subscription events */ export type SubscriptionEvents = { 'status:change': SubscriptionStatusChangeEvent 'status:ready': SubscriptionStatusEvent<`ready`> 'status:loadingSubset': SubscriptionStatusEvent<`loadingSubset`> unsubscribed: SubscriptionUnsubscribedEvent } /** * Public interface for a collection subscription * Used by sync implementations to track subscription lifecycle */ export interface Subscription extends EventEmitter<SubscriptionEvents> { /** Current status of the subscription */ readonly status: SubscriptionStatus } /** * Cursor expressions for pagination, passed separately from the main `where` clause. * The sync layer can choose to use cursor-based pagination (combining these with the where) * or offset-based pagination (ignoring these and using the `offset` parameter). * * Neither expression includes the main `where` clause - they are cursor-specific only. */ export type CursorExpressions = { /** * Expression for rows greater than (after) the cursor value. * For multi-column orderBy, this is a composite cursor using OR of conditions. * Example for [col1 ASC, col2 DESC] with values [v1, v2]: * or(gt(col1, v1), and(eq(col1, v1), lt(col2, v2))) */ whereFrom: BasicExpression<boolean> /** * Expression for rows equal to the current cursor value (first orderBy column only). * Used to handle tie-breaking/duplicates at the boundary. * Example: eq(col1, v1) or for Dates: and(gte(col1, v1), lt(col1, v1+1ms)) */ whereCurrent: BasicExpression<boolean> /** * The key of the last item that was loaded. * Can be used by sync layers for tracking or deduplication. */ lastKey?: string | number } export type LoadSubsetOptions = { /** The where expression to filter the data (does NOT include cursor expressions) */ where?: BasicExpression<boolean> /** The order by clause to sort the data */ orderBy?: OrderBy /** The limit of the data to load */ limit?: number /** * Cursor expressions for cursor-based pagination. * These are separate from `where` - the sync layer should combine them if using cursor-based pagination. * Neither expression includes the main `where` clause. */ cursor?: CursorExpressions /** * Row offset for offset-based pagination. * The sync layer can use this instead of `cursor` if it prefers offset-based pagination. */ offset?: number /** * The subscription that triggered the load. * Advanced sync implementations can use this for: * - LRU caching keyed by subscription * - Reference counting to track active subscriptions * - Subscribing to subscription events (e.g., finalization/unsubscribe) * @optional Available when called from CollectionSubscription, may be undefined for direct calls */ subscription?: Subscription } export type LoadSubsetFn = (options: LoadSubsetOptions) => true | Promise<void> export type UnloadSubsetFn = (options: LoadSubsetOptions) => void export type CleanupFn = () => void export type SyncConfigRes = { cleanup?: CleanupFn loadSubset?: LoadSubsetFn unloadSubset?: UnloadSubsetFn } export interface SyncConfig< T extends object = Record<string, unknown>, TKey extends string | number = string | number, > { sync: (params: { collection: Collection<T, TKey, any, any, any> begin: () => void write: (message: ChangeMessageOrDeleteKeyMessage<T, TKey>) => void commit: () => void markReady: () => void truncate: () => void }) => void | CleanupFn | SyncConfigRes /** * Get the sync metadata for insert operations * @returns Record containing relation information */ getSyncMetadata?: () => Record<string, unknown> /** * The row update mode used to sync to the collection. * @default `partial` * @description * - `partial`: Updates contain only the changes to the row. * - `full`: Updates contain the entire row. */ rowUpdateMode?: `partial` | `full` } export interface ChangeMessage< T extends object = Record<string, unknown>, TKey extends string | number = string | number, > { key: TKey value: T previousValue?: T type: OperationType metadata?: Record<string, unknown> } export type DeleteKeyMessage<TKey extends string | number = string | number> = Omit<ChangeMessage<any, TKey>, `value` | `previousValue` | `type`> & { type: `delete` } export type ChangeMessageOrDeleteKeyMessage< T extends object = Record<string, unknown>, TKey extends string | number = string | number, > = Omit<ChangeMessage<T>, `key`> | DeleteKeyMessage<TKey> export type OptimisticChangeMessage< T extends object = Record<string, unknown>, TKey extends string | number = string | number, > = | (ChangeMessage<T> & { // Is this change message part of an active transaction. Only applies to optimistic changes. isActive?: boolean }) | (DeleteKeyMessage<TKey> & { // Is this change message part of an active transaction. Only applies to optimistic changes. isActive?: boolean }) /** * The Standard Schema interface. * This follows the standard-schema specification: https://github.com/standard-schema/standard-schema */ export type StandardSchema<T> = StandardSchemaV1 & { '~standard': { types?: { input: T output: T } } } /** * Type alias for StandardSchema */ export type StandardSchemaAlias<T = unknown> = StandardSchema<T> export interface OperationConfig { metadata?: Record<string, unknown> /** Whether to apply optimistic updates immediately. Defaults to true. */ optimistic?: boolean } export interface InsertConfig { metadata?: Record<string, unknown> /** Whether to apply optimistic updates immediately. Defaults to true. */ optimistic?: boolean } export type UpdateMutationFnParams< T extends object = Record<string, unknown>, TKey extends string | number = string | number, TUtils extends UtilsRecord = UtilsRecord, > = { transaction: TransactionWithMutations<T, `update`> collection: Collection<T, TKey, TUtils> } export type InsertMutationFnParams< T extends object = Record<string, unknown>, TKey extends string | number = string | number, TUtils extends UtilsRecord = UtilsRecord, > = { transaction: TransactionWithMutations<T, `insert`> collection: Collection<T, TKey, TUtils> } export type DeleteMutationFnParams< T extends object = Record<string, unknown>, TKey extends string | number = string | number, TUtils extends UtilsRecord = UtilsRecord, > = { transaction: TransactionWithMutations<T, `delete`> collection: Collection<T, TKey, TUtils> } export type InsertMutationFn< T extends object = Record<string, unknown>, TKey extends string | number = string | number, TUtils extends UtilsRecord = UtilsRecord, TReturn = any, > = (params: InsertMutationFnParams<T, TKey, TUtils>) => Promise<TReturn> export type UpdateMutationFn< T extends object = Record<string, unknown>, TKey extends string | number = string | number, TUtils extends UtilsRecord = UtilsRecord, TReturn = any, > = (params: UpdateMutationFnParams<T, TKey, TUtils>) => Promise<TReturn> export type DeleteMutationFn< T extends object = Record<string, unknown>, TKey extends string | number = string | number, TUtils extends UtilsRecord = UtilsRecord, TReturn = any, > = (params: DeleteMutationFnParams<T, TKey, TUtils>) => Promise<TReturn> /** * Collection status values for lifecycle management * @example * // Check collection status * if (collection.status === "loading") { * console.log("Collection is loading initial data") * } else if (collection.status === "ready") { * console.log("Collection is ready for use") * } * * @example * // Status transitions * // idle → loading → ready (when markReady() is called) * // Any status can transition to → error or cleaned-up */ export type CollectionStatus = /** Collection is created but sync hasn't started yet (when startSync config is false) */ | `idle` /** Sync has started and is loading data */ | `loading` /** Collection has been explicitly marked ready via markReady() */ | `ready` /** An error occurred during sync initialization */ | `error` /** Collection has been cleaned up and resources freed */ | `cleaned-up` export type SyncMode = `eager` | `on-demand` export interface BaseCollectionConfig< T extends object = Record<string, unknown>, TKey extends string | number = string | number, // Let TSchema default to `never` such that if a user provides T explicitly and no schema // then TSchema will be `never` otherwise if it would default to StandardSchemaV1 // then it would conflict with the overloads of createCollection which // requires either T to be provided or a schema to be provided but not both! TSchema extends StandardSchemaV1 = never, TUtils extends UtilsRecord = UtilsRecord, TReturn = any, > { // If an id isn't passed in, a UUID will be // generated for it. id?: string schema?: TSchema /** * Function to extract the ID from an object * This is required for update/delete operations which now only accept IDs * @param item The item to extract the ID from * @returns The ID string for the item * @example * // For a collection with a 'uuid' field as the primary key * getKey: (item) => item.uuid */ getKey: (item: T) => TKey /** * Time in milliseconds after which the collection will be garbage collected * when it has no active subscribers. Defaults to 5 minutes (300000ms). */ gcTime?: number /** * Whether to eagerly start syncing on collection creation. * When true, syncing begins immediately. When false, syncing starts when the first subscriber attaches. * * Note: Even with startSync=true, collections will pause syncing when there are no active * subscribers (typically when components querying the collection unmount), resuming when new * subscribers attach. This preserves normal staleTime/gcTime behavior. * * @default false */ startSync?: boolean /** * Auto-indexing mode for the collection. * When enabled, indexes will be automatically created for simple where expressions. * @default "eager" * @description * - "off": No automatic indexing * - "eager": Automatically create indexes for simple where expressions in subscribeChanges (default) */ autoIndex?: `off` | `eager` /** * Optional function to compare two items. * This is used to order the items in the collection. * @param x The first item to compare * @param y The second item to compare * @returns A number indicating the order of the items * @example * // For a collection with a 'createdAt' field * compare: (x, y) => x.createdAt.getTime() - y.createdAt.getTime() */ compare?: (x: T, y: T) => number /** * The mode of sync to use for the collection. * @default `eager` * @description * - `eager`: syncs all data immediately on preload * - `on-demand`: syncs data in incremental snapshots when the collection is queried * The exact implementation of the sync mode is up to the sync implementation. */ syncMode?: SyncMode /** * Optional asynchronous handler function called before an insert operation * @param params Object containing transaction and collection information * @returns Promise resolving to any value * @example * // Basic insert handler * onInsert: async ({ transaction, collection }) => { * const newItem = transaction.mutations[0].modified * await api.createTodo(newItem) * } * * @example * // Insert handler with multiple items * onInsert: async ({ transaction, collection }) => { * const items = transaction.mutations.map(m => m.modified) * await api.createTodos(items) * } * * @example * // Insert handler with error handling * onInsert: async ({ transaction, collection }) => { * try { * const newItem = transaction.mutations[0].modified * const result = await api.createTodo(newItem) * return result * } catch (error) { * console.error('Insert failed:', error) * throw error // This will cause the transaction to fail * } * } * * @example * // Insert handler with metadata * onInsert: async ({ transaction, collection }) => { * const mutation = transaction.mutations[0] * await api.createTodo(mutation.modified, { * source: mutation.metadata?.source, * timestamp: mutation.createdAt * }) * } */ onInsert?: InsertMutationFn<T, TKey, TUtils, TReturn> /** * Optional asynchronous handler function called before an update operation * @param params Object containing transaction and collection information * @returns Promise resolving to any value * @example * // Basic update handler * onUpdate: async ({ transaction, collection }) => { * const updatedItem = transaction.mutations[0].modified * await api.updateTodo(updatedItem.id, updatedItem) * } * * @example * // Update handler with partial updates * onUpdate: async ({ transaction, collection }) => { * const mutation = transaction.mutations[0] * const changes = mutation.changes // Only the changed fields * await api.updateTodo(mutation.original.id, changes) * } * * @example * // Update handler with multiple items * onUpdate: async ({ transaction, collection }) => { * const updates = transaction.mutations.map(m => ({ * id: m.key, * changes: m.changes * })) * await api.updateTodos(updates) * } * * @example * // Update handler with optimistic rollback * onUpdate: async ({ transaction, collection }) => { * const mutation = transaction.mutations[0] * try { * await api.updateTodo(mutation.original.id, mutation.changes) * } catch (error) { * // Transaction will automatically rollback optimistic changes * console.error('Update failed, rolling back:', error) * throw error * } * } */ onUpdate?: UpdateMutationFn<T, TKey, TUtils, TReturn> /** * Optional asynchronous handler function called before a delete operation * @param params Object containing transaction and collection information * @returns Promise resolving to any value * @example * // Basic delete handler * onDelete: async ({ transaction, collection }) => { * const deletedKey = transaction.mutations[0].key * await api.deleteTodo(deletedKey) * } * * @example * // Delete handler with multiple items * onDelete: async ({ transaction, collection }) => { * const keysToDelete = transaction.mutations.map(m => m.key) * await api.deleteTodos(keysToDelete) * } * * @example * // Delete handler with confirmation * onDelete: async ({ transaction, collection }) => { * const mutation = transaction.mutations[0] * const shouldDelete = await confirmDeletion(mutation.original) * if (!shouldDelete) { * throw new Error('Delete cancelled by user') * } * await api.deleteTodo(mutation.original.id) * } * * @example * // Delete handler with optimistic rollback * onDelete: async ({ transaction, collection }) => { * const mutation = transaction.mutations[0] * try { * await api.deleteTodo(mutation.original.id) * } catch (error) { * // Transaction will automatically rollback optimistic changes * console.error('Delete failed, rolling back:', error) * throw error * } * } */ onDelete?: DeleteMutationFn<T, TKey, TUtils, TReturn> /** * Specifies how to compare data in the collection. * This should be configured to match data ordering on the backend. * E.g., when using the Electric DB collection these options * should match the database's collation settings. */ defaultStringCollation?: StringCollationConfig utils?: TUtils } export interface CollectionConfig< T extends object = Record<string, unknown>, TKey extends string | number = string | number, TSchema extends StandardSchemaV1 = never, TUtils extends UtilsRecord = UtilsRecord, > extends BaseCollectionConfig<T, TKey, TSchema, TUtils> { sync: SyncConfig<T, TKey> } export type SingleResult = { singleResult: true } export type NonSingleResult = { singleResult?: never } export type MaybeSingleResult = { /** * If enabled the collection will return a single object instead of an array */ singleResult?: true } // Only used for live query collections export type CollectionConfigSingleRowOption< T extends object = Record<string, unknown>, TKey extends string | number = string | number, TSchema extends StandardSchemaV1 = never, TUtils extends UtilsRecord = {}, > = CollectionConfig<T, TKey, TSchema, TUtils> & MaybeSingleResult export type ChangesPayload<T extends object = Record<string, unknown>> = Array< ChangeMessage<T> > /** * An input row from a collection */ export type InputRow = [unknown, Record<string, unknown>] /** * A keyed stream is a stream of rows * This is used as the inputs from a collection to a query */ export type KeyedStream = IStreamBuilder<InputRow> /** * Result stream type representing the output of compiled queries * Always returns [key, [result, orderByIndex]] where orderByIndex is undefined for unordered queries */ export type ResultStream = IStreamBuilder<[unknown, [any, string | undefined]]> /** * A namespaced row is a row withing a pipeline that had each table wrapped in its alias */ export type NamespacedRow = Record<string, Record<string, unknown>> /** * A keyed namespaced row is a row with a key and a namespaced row * This is the main representation of a row in a query pipeline */ export type KeyedNamespacedRow = [unknown, NamespacedRow] /** * A namespaced and keyed stream is a stream of rows * This is used throughout a query pipeline and as the output from a query without * a `select` clause. */ export type NamespacedAndKeyedStream = IStreamBuilder<KeyedNamespacedRow> /** * Options for subscribing to collection changes */ export interface SubscribeChangesOptions { /** Whether to include the current state as initial changes */ includeInitialState?: boolean /** Pre-compiled expression for filtering changes */ whereExpression?: BasicExpression<boolean> } export interface SubscribeChangesSnapshotOptions extends Omit< SubscribeChangesOptions, `includeInitialState` > { orderBy?: OrderBy limit?: number } /** * Options for getting current state as changes */ export interface CurrentStateAsChangesOptions { /** Pre-compiled expression for filtering the current state */ where?: BasicExpression<boolean> orderBy?: OrderBy limit?: number optimizedOnly?: boolean } /** * Function type for listening to collection changes * @param changes - Array of change messages describing what happened * @example * // Basic change listener * const listener: ChangeListener = (changes) => { * changes.forEach(change => { * console.log(`${change.type}: ${change.key}`, change.value) * }) * } * * collection.subscribeChanges(listener) * * @example * // Handle different change types * const listener: ChangeListener<Todo> = (changes) => { * for (const change of changes) { * switch (change.type) { * case 'insert': * addToUI(change.value) * break * case 'update': * updateInUI(change.key, change.value, change.previousValue) * break * case 'delete': * removeFromUI(change.key) * break * } * } * } */ export type ChangeListener< T extends object = Record<string, unknown>, TKey extends string | number = string | number, > = (changes: Array<ChangeMessage<T, TKey>>) => void // Adapted from https://github.com/sindresorhus/type-fest // MIT License Copyright (c) Sindre Sorhus type BuiltIns = | null | undefined | string | number | boolean | symbol | bigint | void | Date | RegExp type HasMultipleCallSignatures< T extends (...arguments_: Array<any>) => unknown, > = T extends { (...arguments_: infer A): unknown (...arguments_: infer B): unknown } ? B extends A ? A extends B ? false : true : true : false type WritableMapDeep<MapType extends ReadonlyMap<unknown, unknown>> = MapType extends ReadonlyMap<infer KeyType, infer ValueType> ? Map<WritableDeep<KeyType>, WritableDeep<ValueType>> : MapType type WritableSetDeep<SetType extends ReadonlySet<unknown>> = SetType extends ReadonlySet<infer ItemType> ? Set<WritableDeep<ItemType>> : SetType type WritableObjectDeep<ObjectType extends object> = { -readonly [KeyType in keyof ObjectType]: WritableDeep<ObjectType[KeyType]> } type WritableArrayDeep<ArrayType extends ReadonlyArray<unknown>> = ArrayType extends readonly [] ? [] : ArrayType extends readonly [...infer U, infer V] ? [...WritableArrayDeep<U>, WritableDeep<V>] : ArrayType extends readonly [infer U, ...infer V] ? [WritableDeep<U>, ...WritableArrayDeep<V>] : ArrayType extends ReadonlyArray<infer U> ? Array<WritableDeep<U>> : ArrayType extends Array<infer U> ? Array<WritableDeep<U>> : ArrayType export type WritableDeep<T> = T extends BuiltIns ? T : T extends (...arguments_: Array<any>) => unknown ? {} extends WritableObjectDeep<T> ? T : HasMultipleCallSignatures<T> extends true ? T : ((...arguments_: Parameters<T>) => ReturnType<T>) & WritableObjectDeep<T> : T extends ReadonlyMap<unknown, unknown> ? WritableMapDeep<T> : T extends ReadonlySet<unknown> ? WritableSetDeep<T> : T extends ReadonlyArray<unknown> ? WritableArrayDeep<T> : T extends object ? WritableObjectDeep<T> : unknown export type MakeOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>