anticrisis
anticrisis

Reputation: 977

In Typescript, define a type for an array where the first element is more specific than the rest

I would like to define a type for an array whose first element is a specific type (e.g. Function), and the remaining elements are the empty type. For example:

type FAs = [Function, {}, {}, {}, ...]; // pseudo code

Is such a thing possible?

The purpose is to provide a single-argument function like this:

const myCaller = ([fun, ...args]: FAs) => fun.apply(args);

An alternative approach would be to use two arguments to myCaller, like this:

const myCaller = (fun: Function, args: any[]) => fun.apply(args);

but for aesthetic reasons I would prefer to use a single argument. I also wonder if the type system supports what is arguably an arbitrary-length tuple. Maybe such a thing is undesirable for computer science reasons I don't understand.

Upvotes: 59

Views: 35114

Answers (6)

Dominique Garmier
Dominique Garmier

Reputation: 140

Assuming you want an Array with at least two elements the first one being of type A and all after that of type B. A more general approach would be to define an intersection type as follow:

type A = string
type B = number

// ensures that there are at least two elements
interface Foo {
  0: A;
  1: B;
}

// ensures that the first element is of type A and any after that of type B
type Bar = [A, ...B[]]

type MyArrayType = Foo & Bar

let a: MyArrayType

a = ['hello world', 1] // works
a = ['hello world', 1, 2] // works
a = ['hello world'] // fails
a = ['hello', 'world'] // fails

Note this works even if A is not a subset of B. Which would be required for the accepted answer to work.

I realize that this constraint was not required by the original question. But since I came across this thread with those constraints it might help someone else.

Upvotes: 6

Nghiệp
Nghiệp

Reputation: 4718

type First<T> = T extends [infer U, ...any[]] ? U : any;
type F = First<[number, boolean, string]> // number

Upvotes: 3

ford04
ford04

Reputation: 74500

I also wonder if the type system supports what is arguably an arbitrary-length tuple.

Since TS 4.0, you can use variadic tuple types. For example, assert [func, ...<proper func args>] in a type-safe way:

type FAs<A extends unknown[], R> = [(...args: A) => R, ...A]

const myCaller = <A extends unknown[], R>([fn, ...args]: FAs<A, R>) =>
    fn.apply(null, args)
Example:
const fn1 = (a1: string, a2: number) => true

const r1 = myCaller([fn1, "foo", 42]) // OK, r1 has type `boolean`
const r2 = myCaller([fn1, "foo", "bar"]) // error, `bar` has wrong type
const r3 = myCaller([fn1, "foo"]) // error, not enough arguments

Playground

Upvotes: 5

juusaw
juusaw

Reputation: 743

In current versions of Typescript this is possible using array spread:

type FAs = [Function, ...Array<{}>]

It supports any length from 1 to n (the first element is required).

Upvotes: 71

user663031
user663031

Reputation:

If you define

type FAs = [Function, {}];

Then values of type FAs will require a first element of type Function, a second element of type {}, and succeeding elements of Function | {}. That is how TypeScript literal array types work. From the TS docs:

When accessing an element outside the set of known indices, a union type is used instead:

This should do everything you want except for the fact that you will be able to pass in a Function-typed value as the third element etc. of the array. But actually that would be the case anyway, since Function is compatible with {}.

There is no way around this. There is no way in TS to define an array type where the first n elements are of some specific type(s), and there are an arbitrary number of remaining elements of some other specific type.

I also wonder if the type system supports what is arguably an arbitrary-length tuple.

Actually, the type system only supports arbitrary-length tuples. If you say

type Tuple = [number, number];

this type is compatible with any array, of length two or greater, that contains numbers. If you say

type Tuple = [string, number];

this type is compatible with any array, of length two or longer, that has a string as its first element, a number as its second, and either a string or number as its third etc. I would not call the reasons for this behavior "computer-science based"; it's more a matter of what it's feasible for TS to check.

An alternate approach

interface Arglist {
  [index: number]: object;
  0: Function;
}

const a1: Arglist = [func];
const a2: Arglist = [22];                  // fails
const a3: Arglist = [func, "foo"];         // fails
const a4: Arglist = [func, obj];
const a5: Arglist = [func, obj, obj];

Upvotes: 35

dbandstra
dbandstra

Reputation: 1314

I'm pretty sure this is the best you can do as of Typescript 2.3. You can see typings like this in lodash for example.

interface IMyCaller {
  <R>([fn]: [() => R]): R;
  <R,A>([fn, a]: [(a: A) => R, A]): R;
  <R,A,B>([fn, a, b]: [(a: A, b: B) => R, A, B]): R;
  <R,A,B,C>([fn, a, b, c]: [(a: A, b: B, c: C) => R, A, B, C]): R;
  // keep adding these until you get tired
}

const myCaller: IMyCaller = ([fun, ...args]) => fun.apply(args);

Upvotes: 0

Related Questions