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 withcallback(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 err
and 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?
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
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.
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!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With