Skip to content

unsetPath

Edit

Immutably removes an own property at an object path.

unsetPath.ts
/** A dot-delimited string or an explicit list of property keys. */
export type ObjectPath = string | readonly PropertyKey[];
type KnownNonContainer =
| Date
| RegExp
| Error
| Promise<unknown>
| ReadonlyMap<unknown, unknown>
| ReadonlySet<unknown>
| WeakMap<object, unknown>
| WeakSet<object>
| ArrayBuffer
| SharedArrayBuffer
| ArrayBufferView
| ((...arguments_: never[]) => unknown)
| (abstract new (...arguments_: never[]) => unknown);
type Digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9";
type NonZeroDigit = Exclude<Digit, "0">;
type IsDigits<Text extends string> = Text extends ""
? true
: Text extends `${Digit}${infer Rest}`
? IsDigits<Rest>
: false;
type IsCanonicalArrayIndexText<Text extends string> = Text extends "0"
? true
: Text extends `${NonZeroDigit}${infer Rest}`
? IsDigits<Rest>
: false;
type DigitRank<Value extends Digit> = Value extends "0"
? []
: Value extends "1"
? [0]
: Value extends "2"
? [0, 0]
: Value extends "3"
? [0, 0, 0]
: Value extends "4"
? [0, 0, 0, 0]
: Value extends "5"
? [0, 0, 0, 0, 0]
: Value extends "6"
? [0, 0, 0, 0, 0, 0]
: Value extends "7"
? [0, 0, 0, 0, 0, 0, 0]
: Value extends "8"
? [0, 0, 0, 0, 0, 0, 0, 0]
: [0, 0, 0, 0, 0, 0, 0, 0, 0];
type IsDigitLess<Left extends Digit, Right extends Digit> =
DigitRank<Right> extends [...DigitRank<Left>, ...infer Rest]
? Rest extends []
? false
: true
: false;
type CompareLength<
Left extends string,
Right extends string,
> = Left extends `${Digit}${infer LeftRest}`
? Right extends `${Digit}${infer RightRest}`
? CompareLength<LeftRest, RightRest>
: "greater"
: Right extends `${Digit}${string}`
? "less"
: "equal";
type IsLexicallyAtMost<
Value extends string,
Maximum extends string,
> = Value extends ""
? true
: Value extends `${infer ValueHead extends Digit}${infer ValueTail}`
? Maximum extends `${infer MaximumHead extends Digit}${infer MaximumTail}`
? ValueHead extends MaximumHead
? IsLexicallyAtMost<ValueTail, MaximumTail>
: IsDigitLess<ValueHead, MaximumHead>
: false
: false;
type IsAtMostMaximumArrayIndex<Text extends string> = CompareLength<
Text,
"4294967294"
> extends "less"
? true
: CompareLength<Text, "4294967294"> extends "greater"
? false
: IsLexicallyAtMost<Text, "4294967294">;
type IsArrayIndexText<Text extends string> =
IsCanonicalArrayIndexText<Text> extends true
? IsAtMostMaximumArrayIndex<Text>
: false;
type IsArrayIndexKey<Key> = Key extends number
? number extends Key
? false
: IsArrayIndexText<`${Key}`>
: Key extends string
? IsArrayIndexText<Key>
: false;
type SplitPath<Path extends string> = Path extends ""
? []
: Path extends `${infer Head}.${infer Tail}`
? [Head, ...SplitPath<Tail>]
: [Path];
type PathSegments<Path extends ObjectPath> = Path extends string
? SplitPath<Path>
: Path;
type IsUnion<Value, Whole = Value> = Value extends unknown
? [Whole] extends [Value]
? false
: true
: never;
type IsSingletonPropertyKey<Value extends PropertyKey> =
[Value] extends [never]
? false
: IsUnion<Value> extends true
? false
: {} extends Record<Value, never>
? false
: true;
type IsDynamicSegment<Value extends PropertyKey> =
IsSingletonPropertyKey<Value> extends true ? false : true;
type HasDynamicSegment<Segments extends readonly PropertyKey[]> =
Segments extends readonly [
infer Head extends PropertyKey,
...infer Tail extends PropertyKey[],
]
? IsDynamicSegment<Head> extends true
? true
: HasDynamicSegment<Tail>
: false;
type IsDynamicPath<Path extends ObjectPath> =
IsUnion<Path> extends true
? true
: [Path] extends [string]
? IsDynamicSegment<Extract<Path, string>>
: [Path] extends [readonly PropertyKey[]]
? number extends Extract<Path, readonly PropertyKey[]>["length"]
? true
: IsUnion<
Extract<Path, readonly PropertyKey[]>["length"]
> extends true
? true
: HasDynamicSegment<Extract<Path, readonly PropertyKey[]>>
: true;
type Simplify<T> = { [Key in keyof T]: T[Key] };
type IsAny<T> = 0 extends 1 & T ? true : false;
type MatchingObjectKey<T extends object, Key> = Key extends keyof T
? Key
: Key extends string
? Key extends `${infer NumberKey extends number}`
? `${NumberKey}` extends Key
? NumberKey extends keyof T
? NumberKey
: never
: never
: never
: Key extends number
? `${Key}` extends keyof T
? `${Key}`
: never
: never;
type PropertyAtBranch<T, Key> =
NonNullable<T> extends readonly (infer Item)[]
? Key extends number
? number extends Key
? Item | undefined
: IsArrayIndexKey<Key> extends true
? Item | undefined
: never
: IsArrayIndexKey<Key> extends true
? Item | undefined
: MatchingObjectKey<NonNullable<T>, Key> extends infer Matched
? [Matched] extends [never]
? never
: NonNullable<T>[Extract<Matched, keyof NonNullable<T>>]
: never
: NonNullable<T> extends object
? MatchingObjectKey<NonNullable<T>, Key> extends infer Matched
? [Matched] extends [never]
? never
: NonNullable<T>[Extract<Matched, keyof NonNullable<T>>]
: never
: never;
type PropertyAt<T, Key> = T extends unknown
? PropertyAtBranch<T, Key>
: never;
type UnsetProperty<T, Key extends PropertyKey> = T extends unknown
? T extends readonly (infer Item)[]
? Key extends number
? IsArrayIndexKey<Key> extends true
? Array<Item>
: T
: IsArrayIndexKey<Key> extends true
? Array<Item>
: MatchingObjectKey<T, Key> extends infer Matched
? [Matched] extends [never]
? T
: Simplify<Omit<T, Extract<Matched, keyof T>>>
: T
: T extends object
? MatchingObjectKey<T, Key> extends infer Matched
? [Matched] extends [never]
? T
: Simplify<Omit<T, Extract<Matched, keyof T>>>
: T
: T
: never;
type ReplaceProperty<
T,
Key extends PropertyKey,
Value,
> = T extends object
? MatchingObjectKey<T, Key> extends infer Matched
? [Matched] extends [never]
? T
: {
[Property in keyof T]: Property extends Extract<Matched, keyof T>
? Value
: T[Property];
}
: T
: T;
type UnsetAtSegments<
T,
Segments extends readonly PropertyKey[],
> = Segments extends readonly [
infer Head extends PropertyKey,
...infer Tail extends PropertyKey[],
]
? Tail extends []
? UnsetProperty<T, Head>
: ReplaceProperty<T, Head, UnsetAtSegments<PropertyAt<T, Head>, Tail>>
: T;
type ResolveSegments<T, Segments extends readonly PropertyKey[]> =
Segments extends readonly [
infer Head extends PropertyKey,
...infer Tail extends PropertyKey[],
]
? ResolveSegments<PropertyAt<T, Head>, Tail>
: T;
type IsDefinitelyKnownNonContainer<Value> = [
IsAny<Value>,
] extends [true]
? false
: [Exclude<Value, null | undefined>] extends [never]
? false
: [Exclude<Value, null | undefined>] extends [KnownNonContainer]
? true
: false;
type HasKnownNonContainerTraversal<
T,
Segments extends readonly PropertyKey[],
> = Segments extends readonly [
infer Head extends PropertyKey,
...infer Tail extends PropertyKey[],
]
? Tail extends []
? false
: IsDefinitelyKnownNonContainer<PropertyAt<T, Head>> extends true
? true
: HasKnownNonContainerTraversal<PropertyAt<T, Head>, Tail>
: false;
type IsArrayLengthMutation<T, Path extends ObjectPath> =
PathSegments<Path> extends readonly [
...infer Parent extends PropertyKey[],
infer Last extends PropertyKey,
]
? Last extends "length"
? Extract<ResolveSegments<T, Parent>, readonly unknown[]> extends never
? false
: true
: false
: false;
/** A non-empty unset path that does not target an array's `length`. */
export type ValidUnsetPath<T, Path extends ObjectPath> =
IsDynamicPath<Path> extends true
? Path
: PathSegments<Path> extends readonly []
? never
: IsArrayLengthMutation<T, Path> extends true
? never
: HasKnownNonContainerTraversal<T, PathSegments<Path>> extends true
? never
: Path;
/** A root that is not a statically known non-plain built-in or function. */
export type ValidMutationRoot<T extends object> =
IsAny<T> extends true ? T : T extends KnownNonContainer ? never : T;
/** The immutable result of removing a property at a literal path. */
export type UnsetPathResult<
T extends object,
Path extends ObjectPath,
> = IsDynamicPath<Path> extends true
? object
: UnsetAtSegments<T, PathSegments<Path>>;
const toSegments = (path: ObjectPath): PropertyKey[] => {
const segments =
typeof path === "string" ? (path === "" ? [] : path.split(".")) : [...path];
if (segments.length === 0) {
throw new RangeError("path must contain at least one segment");
}
return segments;
};
const isArrayIndex = (value: PropertyKey): boolean => {
if (typeof value === "symbol") return false;
const number = typeof value === "number" ? value : Number(value);
if (typeof value === "string" && !/^(0|[1-9]\d*)$/.test(value)) return false;
return (
Number.isSafeInteger(number) &&
number >= 0 &&
number < 4_294_967_295
);
};
const isContainer = (
value: unknown
): value is Record<PropertyKey, unknown> => {
if (value === null || typeof value !== "object") return false;
if (Array.isArray(value)) return true;
const prototype = Object.getPrototypeOf(value);
return prototype === Object.prototype || prototype === null;
};
const defineDataProperty = (
object: Record<PropertyKey, unknown>,
key: PropertyKey,
value: unknown
): void => {
Object.defineProperty(object, key, {
value,
enumerable: true,
configurable: true,
writable: true,
});
};
const cloneContainer = (
value: Record<PropertyKey, unknown>,
omittedKey: PropertyKey
): Record<PropertyKey, unknown> => {
const clone = Array.isArray(value)
? new Array(value.length)
: Object.create(Object.getPrototypeOf(value));
for (const key of Reflect.ownKeys(value)) {
if (key === omittedKey || (Array.isArray(value) && key === "length")) {
continue;
}
const descriptor = Object.getOwnPropertyDescriptor(value, key);
if (descriptor !== undefined) Object.defineProperty(clone, key, descriptor);
}
if (Array.isArray(value)) {
Object.defineProperty(
clone,
"length",
Object.getOwnPropertyDescriptor(value, "length")!
);
}
return clone;
};
const defineReplacementProperty = (
object: Record<PropertyKey, unknown>,
source: Record<PropertyKey, unknown>,
key: PropertyKey,
value: unknown
): void => {
const descriptor = Object.getOwnPropertyDescriptor(source, key);
if (descriptor === undefined) {
defineDataProperty(object, key, value);
return;
}
Object.defineProperty(object, key, {
value,
enumerable: descriptor.enumerable ?? false,
configurable: descriptor.configurable ?? false,
writable: "writable" in descriptor ? descriptor.writable : true,
});
};
/** Immutably removes an own property at an object path. */
export const unsetPath = <T extends object, const Path extends ObjectPath>(
object: T & ValidMutationRoot<T>,
path: Path & ValidUnsetPath<T, Path>
): UnsetPathResult<T, Path> => {
if (!isContainer(object)) {
throw new TypeError("object must be a plain object or array");
}
const segments = toSegments(path);
let current: unknown = object;
const ancestors: Array<{
container: Record<PropertyKey, unknown>;
segment: PropertyKey;
}> = [];
for (const [index, segment] of segments.entries()) {
if (
!isContainer(current) ||
!Object.prototype.hasOwnProperty.call(current, segment)
) {
return object as unknown as UnsetPathResult<T, Path>;
}
if (
index === segments.length - 1 &&
Array.isArray(current) &&
segment === "length"
) {
throw new TypeError("path cannot mutate an array's length");
}
ancestors.push({ container: current, segment });
current = Reflect.get(current, segment);
if (
index < segments.length - 1 &&
current !== null &&
(typeof current === "object" || typeof current === "function") &&
!isContainer(current)
) {
throw new TypeError("path cannot traverse a non-plain object");
}
}
let child: Record<PropertyKey, unknown> | undefined;
for (let index = ancestors.length - 1; index >= 0; index -= 1) {
const { container, segment } = ancestors[index]!;
const clone = cloneContainer(container, segment);
if (child !== undefined) {
defineReplacementProperty(clone, container, segment, child);
} else if (Array.isArray(clone) && isArrayIndex(segment)) {
clone.splice(Number(segment), 1);
} else {
Reflect.deleteProperty(clone, segment);
}
child = clone;
}
return child as UnsetPathResult<T, Path>;
};
Terminal
wget -O src/lib/unsetPath.ts https://raw.githubusercontent.com/jrTilak/lazykit/HEAD/registry/functions/unsetPath.ts
unsetPath.example.ts
import { unsetPath } from "./unsetPath";
const publicUser = unsetPath(
{ user: { name: "Ada", token: "secret" } },
"user.token"
);
  • object (T) — Plain-object or array root.
  • path (Path) — Non-empty dot path or explicit segment tuple.

UnsetPathResult<T, Path> — An immutable type with the literal target property removed. A widened dynamic path returns the conservative object type. Missing paths return the original object by reference.

Broad, union, branded, and template-pattern segments, plus unions of tuple shapes, are dynamic and return object, because the type cannot know which property was removed.

Array indexes from 0 through 4294967294 are spliced without leaving holes; larger, negative, fractional, NaN, and infinite numeric keys are ordinary properties. Empty paths and paths targeting an array’s non-configurable length property are rejected. A non-plain root or intermediate object throws TypeError. Own special keys such as __proto__ are traversed and copied as safe data properties.

Known built-ins and functions are rejected statically as roots and as literal intermediate segments; removing one at the final segment remains valid. Interfaces and custom classes retain runtime validation. Untouched own properties, including non-enumerable and symbol properties, preserve their descriptors.