I am trying to define the type for a property which is a function. This function can accept any arguments (and any number of args), but must return a string.
I'm aware that I can type such a function like this:
(...args: never[]) => string
However this results in no type safety or type hints.
Example (Playground Link)
type Foo = (...args: never[]) => string
const alpha: Foo = () => `no arguments`
alpha() // No type hints, which is expected in this case
const bravo: Foo = (a: string) => `uses one argument${a}`
bravo(/* should only allow 1 string argument */) // Instead gives the type hint: `bar(...args: never[]): string`
const charlie: Foo = (a: number, b: string) => `uses two arguments: ${a} and ${b}`
charlie(/* should expect 2 arguments, a number and a string */) // Instead gives the type hint: `bar(...args: never[]): string`
That is, when typing the open parenthesis after the function name, it should hint at which arguments this particular function accepts.
CodePudding user response:
Your Foo
type accepts any string
-returning function. But when you assign a value to a variable of type Foo
, the compiler does not track the particular value you've assigned. In some sense the compiler treats alpha
, bravo
, and charlie
, as opaque boxes labeled Foo
, and that's all it knows about the contents. So while it's really easy to provide a value of type Foo
, it's almost impossible to consume one.
This forgetfulness is the general behavior for variables of non-union types. For union types, there is assignment narrowing where the appearent type of a variable will be narrowed based on the inferred type of the assigned value. That lets you write const x: string | undefined = "hello"; x.toUpperCase();
without error, since x
is narrowed from string | undefined
to string
by the assignment. It would be nice if this worked in some way for non-unions, and there is a longstanding request at microsoft/TypeScript#16976 to do this, but I don't see any indication this will ever be implemented (it would be a big breaking change to do so). You can't plausibly represent Foo
as a union type, and unions of functions have other complicating behavior anyway, so this is not a feasible solution for you.
Instead, my suggestion is that you really want something like the so-called satisfies
operator, as discussed in microsoft/TypeScript#47920 and originally microsoft/TypeScript#7481. You want alpha
and bravo
and charlie
to satisfy the Foo
type without being widened to the Foo
type. There's no built-in operator that works this way, but you can implement a generic identity helper function that gives you this behavior (this workaround is discussed in those GitHub issues):
type Foo = (...args: never) => string
const asFoo = <F extends Foo>(f: F) => f;
Instead of annotating variables like const x: Foo = ...
, you use the helper function like const x = asFoo(...)
. Here's how it works:
const alpha = asFoo(() => `no arguments`);
console.log(alpha()) // okay
const bravo = asFoo((a: string) => `uses one argument${a}`);
bravo() // error, argument of type sting expected
const charlie = asFoo((a: number, b: string) => `uses two arguments: ${a} and ${b}`);
charlie() // error, two arguments expected
Looks good!
Note that if you actually want to use Foo
objects programmatically you will need to keep track of the arguments list types by using generics, like this:
type GenericFoo<A extends any[]> = (...args: A) => string;
function useFoo<A extends any[]>(foo: GenericFoo<A>, ...args: A): string {
return foo(...args);
}
useFoo(alpha); // okay
useFoo(bravo, "a"); // okay
useFoo(bravo, 123); // error, number is not string
useFoo(bravo); // error, expect another argument