Home > other >  Keep type hints for a TypeScript function that can take any arguments
Keep type hints for a TypeScript function that can take any arguments

Time:08-01

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

Playground link to code

  • Related