2

I'm dealing with classic node callbacks. Example:

myFunction('foo', (err: Error|null, data?: Buffer) =>{
  if (err) {
    // typeof err is Error
    // typeof data is Buffer|undefined
  } else {
    // typeof err is null;
    // typeof data is Buffer|undefined;
  }
});

I am trying to define my own callback-accepting function myFunction. I am trying to achieve two things:

  1. I want to be able to infer the type of data based on the type of err
  2. I want to be able to implicitly infer the types of the input parameters

Example:

// types of err and data are inferred from readFile
readFile('foo', (err, data) => {
  if (err) {
    // typeof err is Error
    // typeof data is undefined
  } else {
    // typeof err is null
    // typeof data is Buffer
  }
}

Is there any way to achieve either of these in current typescript?

1 Answer 1

4

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

Sign up to request clarification or add additional context in comments.

2 Comments

Or may be even type Param = Error | Buffer;
Thank you for the response! Unfortunately, the project I am working on has a lot of legacy code that cannot be converted to a discriminated union. But I will certainly keep it in mind for future work.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.