3

Is it possible to type a callback function in Typescript that encodes the following 2 invariants in the type system:

  1. if err == null then x is of type T
  2. if err != null then x must not be referenced (never?)
type Callback<T> = {
  (error: null, value: T): void;
  (error: Error): void;
};

// The function that takes the typed callback.
function incrementAsync(x: number, cb: Callback<number>): void {
  if (x === 0) {
    return cb(new Error());
  }

  cb(null, x + 1); // should type-check
  cb(new Error()); // should type-check

  cb(new Error(), 1) // should fail type-check
  cb(null);          // should fail type-check
}

Here's the function signature that I really want:

// Error: This overload signature is not compatible 
//        with its implementation signature.
function definedCb1(e: Error);
function definedCb1(e: null, x: number): void {}
incrementAsync(10, definedCb1); // error with strictNullChecks

Here's a looser function signature that type checks

// BAD because types are too loose but this passes type-check.
function definedCb2(e: Error|null, x?: number): void {
  if (e === null) {
    // Is there a way to type this function, so the ! is not necessary?
    console.log(x! + 1);
    return;
  }
  // error because x might be undefined.
  console.log(x + 2);
}
incrementAsync(20, definedCb2) // Works

Typescript Playground Link enable strictNullChecks and noImplicitAny.

3
  • function logNum(error: Error | null, x?: number): void { ... Commented Apr 12, 2019 at 23:08
  • and change if (error == null) by if (error !== null) if i understand what you want to do Commented Apr 12, 2019 at 23:31
  • I edited the question for added detail: Is there a way to add types to a function so that either: 1. err == null and x has type T or 2. err != null and x must not be referenced. Commented Apr 15, 2019 at 4:14

1 Answer 1

1

If you turn on strictNullChecks, then this meets your requirements.

interface Callback<T> {
  (error: Error): void;
  (error: null, value: T): void;
}

function incrementAsync(x: number, cb: Callback<number>): void {
  cb(new Error());          // works
  cb(null, x + 1);          // works

  cb(null);                 // error
  cb(new Error(), 'foo');   // error
  cb(null, 'foo');          // error
  cb(new Error(), 10);      // error    
}

Here it is in the playground.

The strictNullChecks flag prevents all types from being nullable types.

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

4 Comments

Right, but I'd like to encode the invariant that if error == null, then value is T into the type system. The simpler Callback is too loose.
The simple callback has that invariant already, because value must be T whether error is null or not. I have added use cases to my answer, and am interested to hear other ones that you might have.
In the simple callback,the value with type T is optional. I'd like to enforce that the value is T and is present if error == null. So, cb(new Error(), 10); should error and so should cb(null);
Thanks for your patience. Your approach works for type-checking inside the function that calls the callback. My difficulty is creating a function that matches the same type-level invariants as Callback<T>. I've added more detail to the original question to show the difficulty.

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.