Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript error-first callback typing (noImplicitAny, strictNullChecks)

I want to declare a function with a error-first callback in a project using typscript with noImplicitAny and strictNullChecks.

Is there a way to declare a interface or type that allows two different signatures and works when calling?

The simplest and most straightforward way is to declare it as such

function example(callback: (err?: Error, result?: {data: any}) => void) {
    callback(undefined, {data: "hello"});
}

This, however, allows me to call callback() (without paramters) inside example which is not something we want to do as the callback should always be called with either an error or a result.

function example(callback: (err: Error | undefined, result:  {data: any} | undefined) => void) {
    callback(undefined, {data: "hello"});
}

This does not allow example(). The callback must be called with 2 parameters.

Both of these patterns however mean that both err and result can be undefined. This is not perfect as the following will cause an error.

example((err, result) => {
    if(err) { console.error(err); return; }

    console.log(result.data);
});

Since result can be undefined, we can't assume that it has property data. To fix this I either have to assert that the second parameter is something when calling, example((err, result: {data: any}) => void), or wrap any interaction with result in if(result)


I want to declare that the callback will always be called either with
callback(undefined, { data: "Hello" }) or callback(Error, undefined). Both parameters will never be undefined.

The working way i found to declare this is

interface ICallback<r> {
    (err: undefined, result: r): void;
    (err: Error, result: undefined): void;
};

function example(callback: ICallback<{data: any}>) {
    callback(undefined, {data: "hello"});
}

This seems to work as, inside example, calling callback(new Error(), {data: "error"}) or callback(undefined, undefined) will cause an error.

However; when I'm then using this example function;

example((err, result) => {
    ...
});

Both errand result are implicitly any. Is there any way to declare the callback function that allows for the (undefined, ISomething) or (ISomethingElse, undefined) signatures which should mean that we can expect parameter2 to be defined if parameter1 is undefined?

like image 458
oBusk Avatar asked Mar 05 '17 02:03

oBusk


2 Answers

however, allows me to call example(() => void) which is not really something I want as we should handle atleast the error.

This will always be the case. A callback is free to ignore any arguments.

This is because of TypeScript's function compatability : https://basarat.gitbooks.io/typescript/content/docs/types/type-compatibility.html#number-of-arguments

More

In a way this is similar to how you are free to ignore catching exceptions. So a callback is free to ignore handling the error if it so wants.

like image 191
basarat Avatar answered Sep 21 '22 23:09

basarat


The closest I've gotten is:

interface NodeCallback<T> {
  (err: any, result?: undefined | null): void;
  (err: undefined | null, result: T): void;
}

For a cb: NodeCallback<string>, this allows:

  • cb('boom'),
  • cb(new Error('boom')),
  • cb('boom', null),
  • cb('boom', undefined),
  • cb(null, 'box'),
  • cb(undefined, 'box')

but not cb(new Error('boom'), 'box'), cb(null, 123), etc.

Unfortunately, it's only good on the consumer side (that is, the one calling the callback) - typescript will give up on contextually inferring parameter types when it hits an overload signature (or, equivalently, an intersection of signatures):

// 'err' and 'result' have type 'any', which will error with --strict / --noImplicitAny
const cb: NodeCallback<number> = (err, result) => {};

Fortunately there is a recently opened PR that may fix this!

like image 24
Simon Buchan Avatar answered Sep 20 '22 23:09

Simon Buchan