Conventionally you'd use a single parameter of a discriminated union type instead of two union-typed parameters like err and data whose types are correlated to each other.
TypeScript really doesn't have much support for correlated expressions; see microsoft/TypeScript#30581. That is, there isn't a great way to tell the compiler that while err is of type Error | null and while data is of type Buffer | undefined, some combinations of the types for err and data are not possible. You can use control flow analysis to check the type of err, but it will have no effect on the perceived type of data... the compiler erroneously assumes they are independent expressions.
Here is the closest I can get; it makes heavy use of tuples in rest and spread expressions and you really have to fight with the compiler to get it to sort of happen:
declare function myFunction(
someString: string,
someCallback: (...args: [Error, undefined] | [null, Buffer]) => void
): void;
The type of someCallback is a function of exactly two arguments, which are either the types [Error, undefined] or the typed [null, Buffer]. Now you can call it with a two-param callback, but you'll run into the exact same problem you already have: checking err does not do anything to data:
// can't call it this way
myFunction("oops", (err, data) => {
err; // Error | null
data; // Buffer | undefined
if (err) {
data; // still Buffer | undefined 😟
}
});
Instead you have to use rest parameters in the callback implementation also:
myFunction("foo", (...errData) => {
if (errData[0]) {
const [err, data] = errData;
err; // Error
data; // undefined
} else {
const [err, data] = errData;
err; // null
data; // Buffer
}
});
That works because when you check errData[0] you are checking a single property of a tuple object, and the compiler will use control flow analysis to narrow errData to one of the two known types. You can only break errData out into err and data after the check.
I strongly suggest you consider switching to a single discriminated union parameter in your callback. Look at how easy the solution is:
type Param = { type: "Error"; err: Error } | { type: "Data"; data: Buffer };
declare function myFunction(
someString: string,
someCallback: (param: Param) => void
): void;
myFunction("foo", param => {
if (param.type === "Error") {
param.err; // Error
} else {
param.data; // Buffer
}
});
That's pretty much exactly what you want, and no fighting necessary.
Link to code