Reputation: 977
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
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
Reputation: 4718
type First<T> = T extends [infer U, ...any[]] ? U : any;
type F = First<[number, boolean, string]> // number
Upvotes: 3
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
Upvotes: 5
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
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.
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
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