Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Obtain a "slice" of a Typescript 'Parameters' tuple

Consider the Parameters utility type, the underlying type of which is a tuple: https://www.typescriptlang.org/docs/handbook/utility-types.html#parameterstype

I have a function SomeFunction. To use the function's arguments as a type, I write Parameters<SomeFunction>.

Now let's say I want to use the function's arguments as a type except for the first argument.

Obviously for an array I would use something like ...args.slice(1). But I'm not aware of a slicing utility for Typescript definitions. Omit only works for objects.

An answer to this SO question provides a RemoveFirstFromTuple utility. But it's a bit convoluted. Is there a built-in way of extracting part of a tuple within a type definition?

like image 794
RobertAKARobin Avatar asked Oct 14 '22 21:10

RobertAKARobin


1 Answers

Yes, you can use conditional type inference on the function type, in a way very similar to how the Parameters utility type is implemented:

type ParametersExceptFirst<F> = 
   F extends (arg0: any, ...rest: infer R) => any ? R : never;

compare to

// from lib.es5.d.ts
type Parameters<T extends (...args: any) => any> = 
  T extends (...args: infer P) => any ? P : never;

and verify that it works:

declare function foo(x: string, y: number, z: boolean): Date;
type FooParamsExceptFirst = ParametersExceptFirst<typeof foo>;
// type FooParamsExceptFirst = [y: number, z: boolean]
declare function foo(x: string, y: number, z: boolean): Date;

Playground link to code


UPDATE: arbitrary slicing of tuples with numeric literals is possible, but not pretty and has caveats. First let's write TupleSplit<T, N> which takes a tuple T and a numeric literal type N, and splits the tuple T at index N, returning two pieces: the first piece is the first N elements of T, and the second piece is everything after that. (If N is more than the length of T then the first piece is all of T and the second piece is empty):

type TupleSplit<T, N extends number, O extends readonly any[] = readonly []> =
    O['length'] extends N ? [O, T] : T extends readonly [infer F, ...infer R] ?
    TupleSplit<readonly [...R], N, readonly [...O, F]> : [O, T]

This works via recursive conditional types on variadic tuples and is therefore more computationally intensive than the relatively simple ParametersExceptFirst implementation above. If you try this on long tuples (lengths more than 25 or so) you can expect to see recursion errors. If you try this on ill-behaved types like non-fixed-length tuples or unions of things, you might get weird results. It's fragile; be careful with it.

Let's verify that it works:

type Test = TupleSplit<readonly ["a", "b", "c", "d", "e"], 3>
// type Test = [readonly ["a", "b", "c"], readonly ["d", "e"]]

Looks good.


Now we can use TupleSplit<T, N> to implement TakeFirst<T, N>, returning just the first N elements of T, and SkipFirst<T, N>, which skips the first N elements of T:

type TakeFirst<T extends readonly any[], N extends number> =
    TupleSplit<T, N>[0];

type SkipFirst<T extends readonly any[], N extends number> =
    TupleSplit<T, N>[1];

And finally TupleSlice<T, S, E> produces the slice of tuple T from start position S to end position E (remember, slices are inclusive of the start index, and exclusive of the end index) by taking the first E elements of T and skipping the first S elements of the result:

type TupleSlice<T extends readonly any[], S extends number, E extends number> =
    SkipFirst<TakeFirst<T, E>, S>

To demonstrate that this more or less represents what array slice() does, let's write a function and test it:

function slice<T extends readonly any[], S extends number, E extends number>(
    arr: readonly [...T], start: S, end: E
) {
    return arr.slice(start, end) as readonly any[] as TupleSlice<T, S, E>;
}

const tuple = ["a", "b", "c", "d", "e"] as const
// const tuple: readonly ["a", "b", "c", "d", "e"]

const ret0 = slice(tuple, 2, 4);
// const ret0: readonly ["c", "d"]
console.log(ret0); // ["c", "d"]

const ret1 = slice(tuple, 0, 9);
// const ret1: readonly ["a", "b", "c", "d", "e"]
console.log(ret1); // ["a", "b", "c", "d", "e"];

const ret2 = slice(tuple, 5, 3);
// const ret2: readonly []
console.log(ret2); // [];

This looks good; the returned arrays from slice() have types that accurately represent their values.

Of course, there are many caveats; if you pass negative or non-whole numbers to slice() for S and E, then TupleSlice<T, S, E> is very likely not to correspond to what actually happens with array slices: negative "from end" behavior is possibly implementable but it would be even uglier; non-whole numbers or even just number have not been tested but I expect recursion warnings and other things that go bump in the night. Be warned!

Playground link to code

like image 76
jcalz Avatar answered Oct 20 '22 03:10

jcalz