From 7bf511128e8bd11d8a7966fd3f2a9e7f38ebda3d Mon Sep 17 00:00:00 2001 From: taiyakihitotsu Date: Tue, 10 Feb 2026 06:43:31 +0900 Subject: [PATCH 1/5] fix: `OmitDeep` to preserve type on out-of-bounds indices for fixed-tuples and objects --- source/omit-deep.d.ts | 52 ++++++++++++++++++++++++++++++++++++++++--- test-d/omit-deep.ts | 9 +++++++- 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/source/omit-deep.d.ts b/source/omit-deep.d.ts index d3fd115c1..39d072d9a 100644 --- a/source/omit-deep.d.ts +++ b/source/omit-deep.d.ts @@ -8,6 +8,8 @@ import type {SimplifyDeep} from './simplify-deep.d.ts'; import type {Simplify} from './simplify.d.ts'; import type {UnionToTuple} from './union-to-tuple.d.ts'; import type {UnknownArray} from './unknown-array.d.ts'; +import type {LessThan} from './less-than.d.ts'; +import type {IsTuple} from './is-tuple.d.ts'; /** Omit properties from a deeply-nested object. @@ -131,7 +133,10 @@ It replaces the item to `unknown` at the given index. @example ``` type A = OmitDeepArrayWithOnePath<[10, 20, 30, 40], 2>; -//=> type A = [10, 20, unknown, 40]; +//=> [10, 20, unknown, 40]; + +type B = OmitDeepArrayWithOnePath<[10, 20, 30, 40], 6>; +//=> [10, 20, 30, 40]; ``` */ type OmitDeepArrayWithOnePath = @@ -141,14 +146,55 @@ type OmitDeepArrayWithOnePath, SubPath>> // If `ArrayIndex` is a number literal - : ArraySplice, SubPath>]> + : [true, false] extends [IsTuple, IsValidIndex] + ? ArrayType + : ArraySplice, SubPath>]> // If the path is equal to `number` : P extends `${infer ArrayIndex extends number}` // If `ArrayIndex` is `number` ? number extends ArrayIndex ? [] // If `ArrayIndex` is a number literal - : ArraySplice + : [true, false] extends [IsTuple, IsValidIndex] + ? ArrayType + : ArraySplice : ArrayType; +/** +`IsValidIndex` returns `true` if `Index` (either as a number or its string representation) is a valid index for the given `Array` or `Tuple`. + +If the first argument is a plain `Array`, it accepts `number`. +if it is a `Tuple`, it only accepts specific numeric indices within its bounds. + +@example +``` +type TupleNumberIndex = IsValidIndex<[0, 1, 2], '2'>; +//=> true +type TupleStringIndex = IsValidIndex<[0, 1, 2], 2>; +//=> true +type TupleNumberOutOfIndex = IsValidIndex<[0, 1, 2], 9>; +//=> false +type TupleStringOutOfIndex = IsValidIndex<[0, 1, 2], '9'>; +//=> false +type TupleNumber = IsValidIndex<[0, 1, 2], number>; +//=> false +type ArrayNumberIndex = IsValidIndex; +//=> true +type ArrayStringIndex = IsValidIndex; +//=> true +type ArrayNumber = IsValidIndex; +//=> true +type NotFixedTuple = IsValidIndex<['0', ...number[]], 0>; +//=> true +``` +*/ +type IsValidIndex = + `${Index}` extends `${infer numberString extends number}` + ? false extends IsTuple + ? true + : number extends numberString + ? false + : LessThan + : false; + export {}; diff --git a/test-d/omit-deep.ts b/test-d/omit-deep.ts index 3467943df..ef2c5b94a 100644 --- a/test-d/omit-deep.ts +++ b/test-d/omit-deep.ts @@ -1,5 +1,5 @@ import {expectType} from 'tsd'; -import type {OmitDeep} from '../index.d.ts'; +import type {OmitDeep, IsEqual} from '../index.d.ts'; declare class ClassA { a: string; @@ -141,3 +141,10 @@ expectType<{array: Array<{c: string}>}>(arrayWithMultiplePaths); declare const tupleWithMultiplePaths: OmitDeep<{tuple: [{a: string; b: number; c: string}]}, 'tuple.0.a' | 'tuple.0.b'>; expectType<{tuple: [{c: string}]}>(tupleWithMultiplePaths); + +// Returns the original type unchanged if the specified path does not exist. +type TupleInObject = {obj: {a: [0, 1]; b: {bb: {bbb: 10}}; c: boolean}}; +expectType({} as OmitDeep); + +type ObjectInTuple = {obj: {a: [0, 1, {bb: {bbb: 10}}]; c: [0, 1, ['20', '22']]}}; +expectType({} as OmitDeep); From b80120c17831d62d882ad35ed451e83ae9b9c83f Mon Sep 17 00:00:00 2001 From: taiyakihitotsu Date: Tue, 10 Feb 2026 08:18:01 +0900 Subject: [PATCH 2/5] refactor: remove unused import --- test-d/omit-deep.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-d/omit-deep.ts b/test-d/omit-deep.ts index ef2c5b94a..bff5c6c5e 100644 --- a/test-d/omit-deep.ts +++ b/test-d/omit-deep.ts @@ -1,5 +1,5 @@ import {expectType} from 'tsd'; -import type {OmitDeep, IsEqual} from '../index.d.ts'; +import type {OmitDeep} from '../index.d.ts'; declare class ClassA { a: string; From 90d2591b0cca03c0ce6c0284d966da8e8b81a76e Mon Sep 17 00:00:00 2001 From: taiyakihitotsu Date: Tue, 10 Feb 2026 08:30:32 +0900 Subject: [PATCH 3/5] refactor: update comment --- source/omit-deep.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/omit-deep.d.ts b/source/omit-deep.d.ts index 39d072d9a..d5cd87e6b 100644 --- a/source/omit-deep.d.ts +++ b/source/omit-deep.d.ts @@ -145,7 +145,7 @@ type OmitDeepArrayWithOnePath, SubPath>> - // If `ArrayIndex` is a number literal + // If `ArrayIndex` is a number literal and out-of-bounds, returns the original type. : [true, false] extends [IsTuple, IsValidIndex] ? ArrayType : ArraySplice, SubPath>]> @@ -154,7 +154,7 @@ type OmitDeepArrayWithOnePath, IsValidIndex] ? ArrayType : ArraySplice From db443cd93b79e2575879283f06c5459aa1e7bb80 Mon Sep 17 00:00:00 2001 From: taiyakihitotsu Date: Tue, 10 Feb 2026 10:20:27 +0900 Subject: [PATCH 4/5] fix: `IsValidIndex` return `false` if index is negative number --- source/omit-deep.d.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/source/omit-deep.d.ts b/source/omit-deep.d.ts index d5cd87e6b..21eeead3d 100644 --- a/source/omit-deep.d.ts +++ b/source/omit-deep.d.ts @@ -176,6 +176,8 @@ type TupleNumberOutOfIndex = IsValidIndex<[0, 1, 2], 9>; //=> false type TupleStringOutOfIndex = IsValidIndex<[0, 1, 2], '9'>; //=> false +type TupleNumberOutOfIndexMinus = IsValidIndex<[0, 1, 2], -1>; +//=> false type TupleNumber = IsValidIndex<[0, 1, 2], number>; //=> false type ArrayNumberIndex = IsValidIndex; @@ -194,7 +196,9 @@ type IsValidIndex = ? true : number extends numberString ? false - : LessThan + : true extends LessThan + ? LessThan<-1, numberString> + : false : false; export {}; From b159f3ca316452471fc1be9f7fe2e5f3c6b7a28f Mon Sep 17 00:00:00 2001 From: taiyakihitotsu Date: Tue, 10 Feb 2026 16:47:23 +0900 Subject: [PATCH 5/5] test: update negative number index cases --- test-d/omit-deep.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test-d/omit-deep.ts b/test-d/omit-deep.ts index bff5c6c5e..f4b49d76b 100644 --- a/test-d/omit-deep.ts +++ b/test-d/omit-deep.ts @@ -148,3 +148,12 @@ expectType({} as OmitDeep({} as OmitDeep); + +declare const tupleNegativeIndex: OmitDeep<{tuple: ['a', 'b']}, 'tuple.-1'>; +expectType<{tuple: ['a', 'b']}>(tupleNegativeIndex); + +declare const tupleNegativeNestedIndex: OmitDeep<{tuple: [{x: 1}]}, 'tuple.-1.x'>; +expectType<{tuple: [{x: 1}]}>(tupleNegativeNestedIndex); + +declare const tupleMixedValidAndNegativeIndex: OmitDeep<{tuple: [{x: 1; y: 2}]}, 'tuple.0.x' | 'tuple.-1.y'>; +expectType<{tuple: [{y: 2}]}>(tupleMixedValidAndNegativeIndex);