Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 26 additions & 5 deletions source/array-slice.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {Not, TupleMin} from './internal/index.d.ts';
import type {IsEqual} from './is-equal.d.ts';
import type {And} from './and.d.ts';
import type {ArraySplice} from './array-splice.d.ts';
import type {IsNever} from './is-never.d.ts';

/**
Returns an array slice of a given range, just like `Array#slice()`.
Expand Down Expand Up @@ -60,13 +61,33 @@ export type ArraySlice<
Start extends number = never,
End extends number = never,
> = Array_ extends unknown // To distributive type
? And<IsEqual<Start, never>, IsEqual<End, never>> extends true
? Array_
: number extends Array_['length']
? VariableLengthArraySliceHelper<Array_, Start, End>
: ArraySliceHelper<Array_, IsEqual<Start, never> extends true ? 0 : Start, IsEqual<End, never> extends true ? Array_['length'] : End>
? IsNever<Start> extends true
? IsNever<End> extends true
? _ArraySlice<Array_, Start, End>
: End extends unknown // To distribute `End`
? _ArraySlice<Array_, Start, End>
: never // Never happens
: IsNever<End> extends true
? Start extends unknown // To distribute `Start`
? _ArraySlice<Array_, Start, End>
: never // Never happens
: Start extends unknown // To distribute `Start`
? End extends unknown // To distribute `End`
? _ArraySlice<Array_, Start, End>
: never // Never happens
: never // Never happens
Comment on lines +64 to +78
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is complicated/verbose because the default value of Start/End is never, so we have to explicitly check for never before distributing, which introduces several branching cases.


I guess the reason for having the default value as never was that if End were simply Array_["length"], then union instantiations of End would give incorrect results.

For example,
ArraySlice<[0, 1, 2] | [0, 1], 0> would become
ArraySlice<[0, 1, 2] | [0, 1], 0, 3 | 2>,
which would then distribute to
ArraySlice<[0, 1, 2], 3 | 2> | ArraySlice<[0, 1], 3 | 2>,
and finally to
ArraySlice<[0, 1, 2], 3> | ArraySlice<[0, 1, 2], 2> | ArraySlice<[0, 1], 3> | ArraySlice<[0, 1], 2>,
which would be incorrect.

However, if we set the default for End to UnionMax<Array_["length"]>, that would fix this issue. The only thing is that explicitly passing never would no longer behave the same as omitting the argument. But that’s probably acceptable because never shouldn’t really be a valid value for Start/End anyway. In such cases, we can just return something like readonly unknown[] or [].

Copy link
Collaborator Author

@som-sm som-sm Nov 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, if we set the default for End to UnionMax<Array_["length"]>, that would fix this issue. The only thing is that explicitly passing never would no longer behave the same as omitting the argument. But that’s probably acceptable because never shouldn’t really be a valid value for Start/End anyway. In such cases, we can just return something like readonly unknown[] or [].

@sindresorhus If this is fine, then I'll open a separate PR for this.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, seems fine.

: never; // Never happens

type _ArraySlice<
Array_ extends readonly unknown[],
Start extends number = 0,
End extends number = Array_['length'],
> = And<IsEqual<Start, never>, IsEqual<End, never>> extends true
? Array_
: number extends Array_['length']
? VariableLengthArraySliceHelper<Array_, Start, End>
: ArraySliceHelper<Array_, IsEqual<Start, never> extends true ? 0 : Start, IsEqual<End, never> extends true ? Array_['length'] : End>;

type VariableLengthArraySliceHelper<
Array_ extends readonly unknown[],
Start extends number,
Expand Down
4 changes: 2 additions & 2 deletions source/string-slice.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ StringSlice<'abcde', -2, -1>;
*/
export type StringSlice<
S extends string,
Start extends number = 0,
End extends number = StringToArray<S>['length'],
Start extends number = never,
End extends number = never,
> = string extends S
? string
: ArraySlice<StringToArray<S>, Start, End> extends infer R extends readonly string[]
Expand Down
37 changes: 37 additions & 0 deletions test-d/array-slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,40 @@ expectType<ArraySlice<[1, 2, 3, ...string[], 4, 5], 0>>([1, 2, 3, ...(null! as s
expectType<ArraySlice<[1, 2, 3, ...string[], 4, 5], 1>>([2, 3, ...(null! as string[]), 4, 5]);
expectType<ArraySlice<[1, 2, 3, ...string[], 4, 5], 3>>([...(null! as string[]), 4, 5]);
expectType<ArraySlice<[1, 2, 3, ...string[], 4, 5], 10>>([...(null! as string[]), 4, 5]);

// Unions
// Array is union
expectType<ArraySlice<[0, 1, 2] | ['a', 'b', 'c', 'd'], 0>>({} as [0, 1, 2] | ['a', 'b', 'c', 'd']); // Positive start, no end
expectType<ArraySlice<[0, 1, 2] | ['a', 'b', 'c', 'd'], -2>>({} as [1, 2] | ['c', 'd']); // Negative start, no end
expectType<ArraySlice<[0, 1, 2] | ['a', 'b', 'c', 'd'], 0, 2>>({} as [0, 1] | ['a', 'b']); // Positive start, positive end
expectType<ArraySlice<[0, 1, 2] | ['a', 'b', 'c', 'd'], -2, -1>>({} as [1] | ['c']); // Negative start, negative end
expectType<ArraySlice<[0, 1, 2] | ['a', 'b', 'c', 'd'], -3, 2>>({} as [0, 1] | ['b']); // Negative start, positive end
expectType<ArraySlice<[0, 1, 2] | ['a', 'b', 'c', 'd'], 1, -1>>({} as [1] | ['b', 'c']); // Positive start, negative end

// Start is union
expectType<ArraySlice<[0, 1, 2, 3], 1 | -2>>({} as [1, 2, 3] | [2, 3]); // Positive/Negative start, no end
expectType<ArraySlice<[0, 1, 2, 3], 2 | -3, 3>>({} as [2] | [1, 2]); // Positive/Negative start, positive end
expectType<ArraySlice<[0, 1, 2, 3], 0 | -2, -1>>({} as [2] | [0, 1, 2]); // Positive/Negative start, negative end

// End is union
expectType<ArraySlice<[0, 1, 2, 3], 0, 1 | -2>>({} as [0] | [0, 1]); // Positive start, positive/negative end
expectType<ArraySlice<[0, 1, 2, 3], -2, 2 | -1>>({} as [] | [2]); // Negative start, positive/negative end

// Array and start are unions
expectType<ArraySlice<[0, 1, 2] | ['a', 'b', 'c', 'd'], 1 | -1>>({} as [1, 2] | [2] | ['b', 'c', 'd'] | ['d']); // Positive/Negative start, no end
expectType<ArraySlice<[0, 1, 2] | ['a', 'b', 'c', 'd'], 1 | -2, 2>>({} as [1] | ['b'] | []); // Positive/Negative start, positive end
expectType<ArraySlice<[0, 1, 2] | ['a', 'b', 'c', 'd'], 0 | -2, -1>>({} as [0, 1] | [1] | ['a', 'b', 'c'] | ['c']); // Positive/Negative start, negative end

// Array and end are unions
expectType<ArraySlice<[0, 1, 2] | ['a', 'b', 'c', 'd'], 2, 3 | -1>>({} as [2] | [] | ['c']); // Positive start, positive/negative end
expectType<ArraySlice<[0, 1, 2] | ['a', 'b', 'c', 'd'], -3, 3 | -2>>({} as [0, 1, 2] | [0] | ['b', 'c'] | ['b']); // Negative start, positive/negative end

// Start and end are unions
expectType<ArraySlice<[0, 1, 2, 3], -5 | 0 | 1, -2 | 0 | 3>>( // Positive/Negative start, positive/negative end
{} as [0, 1] | [0, 1, 2] | [] | [1] | [1, 2],
);

// Array, start and end are unions
expectType<ArraySlice<[0, 1, 2] | ['a', 'b', 'c', 'd'], 1 | -4, 4 | -1>>( // Positive/Negative start, positive/negative end
{} as [1] | [1, 2] | [0, 1] | [0, 1, 2] | ['a', 'b', 'c', 'd'] | ['a', 'b', 'c'] | ['b', 'c'] | ['b', 'c', 'd'],
);
37 changes: 37 additions & 0 deletions test-d/string-slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,40 @@ expectType<StringSlice<'abcde', 100, 1>>('');
expectType<StringSlice<string>>(null! as string);
expectType<StringSlice<string, 1>>(null! as string);
expectType<StringSlice<string, 1, 2>>(null! as string);

// Unions
// String is union
expectType<StringSlice<'012' | 'abcd', 0>>({} as '012' | 'abcd'); // Positive start, no end
expectType<StringSlice<'012' | 'abcd', -2>>({} as '12' | 'cd'); // Negative start, no end
expectType<StringSlice<'012' | 'abcd', 0, 2>>({} as '01' | 'ab'); // Positive start, positive end
expectType<StringSlice<'012' | 'abcd', -2, -1>>({} as '1' | 'c'); // Negative start, negative end
expectType<StringSlice<'012' | 'abcd', -3, 2>>({} as '01' | 'b'); // Negative start, positive end
expectType<StringSlice<'012' | 'abcd', 1, -1>>({} as '1' | 'bc'); // Positive start, negative end

// Start is union
expectType<StringSlice<'0123', 1 | -2>>({} as '123' | '23'); // Positive/Negative start, no end
expectType<StringSlice<'0123', 2 | -3, 3>>({} as '2' | '12'); // Positive/Negative start, positive end
expectType<StringSlice<'0123', 0 | -2, -1>>({} as '2' | '012'); // Positive/Negative start, negative end

// End is union
expectType<StringSlice<'0123', 0, 1 | -2>>({} as '0' | '01'); // Positive start, positive/negative end
expectType<StringSlice<'0123', -2, 2 | -1>>({} as '' | '2'); // Negative start, positive/negative end

// Array and start are unions
expectType<StringSlice<'012' | 'abcd', 1 | -1>>({} as '12' | '2' | 'bcd' | 'd'); // Positive/Negative start, no end
expectType<StringSlice<'012' | 'abcd', 1 | -2, 2>>({} as '1' | 'b' | ''); // Positive/Negative start, positive end
expectType<StringSlice<'012' | 'abcd', 0 | -2, -1>>({} as '01' | '1' | 'abc' | 'c'); // Positive/Negative start, negative end

// Array and end are unions
expectType<StringSlice<'012' | 'abcd', 2, 3 | -1>>({} as '2' | '' | 'c'); // Positive start, positive/negative end
expectType<StringSlice<'012' | 'abcd', -3, 3 | -2>>({} as '012' | '0' | 'bc' | 'b'); // Negative start, positive/negative end

// Start and end are unions
expectType<StringSlice<'0123', -5 | 0 | 1, -2 | 0 | 3>>( // Positive/Negative start, positive/negative end
{} as '01' | '012' | '' | '1' | '12',
);

// Array, start and end are unions
expectType<StringSlice<'012' | 'abcd', 1 | -4, 4 | -1>>( // Positive/Negative start, positive/negative end
{} as '1' | '12' | '01' | '012' | 'abcd' | 'abc' | 'bc' | 'bcd',
);