Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to make typesafe node-style callbacks?

Tags:

typescript

Node callbacks look something like:

interface NodeCallback<TResult,TError> {
  (err: TError): void;
  (err: null, res: TResult): void;
}

So the callback will either get err or res but not both. Most of the typings I see have the types of err and res hard coded to their non-optional versions.

function readdir(path: string, callback?: (err: NodeJS.ErrnoException, files: string[]) => void): void;

This isn't strictly typesafe. For example this compiles fine:

fs.readdir('/', (err, files) => {
  if (err !== null) { // There's an error!
    files.forEach(log); // Still using the result just fine.
  }
})

You can make this more (well, kind of) safe by changing the signature to include all possible values.

function readdir(path: string, callback?: (err: null | NodeJS.ErrnoException, files?: string[]) => void): void;

But there's no way to specify the dependency between the two so you need to type assert res to quiet down strictNullChecks.

fs.readdir('/', (err, files) => {
  if (err === null) { // There's no error
    // files.forEach(log); // Won't compile
    (files as string[]).forEach(log); // Type assertion
    files!.forEach(log); // Nice shorthand
    if (files !== undefined) { // Type guard
      files.forEach(log);
    }
  }
})

This is not too bad except for:

  • When you need to do it repeatedly.
  • When you're not accessing a property so you have to type assert, which might mean you need to import another type. Really annoying. Type guards will avoid this but then you've got an unnecessary runtime penalty.
  • It's still not actually safe. It's more in-your-face so you're forced to think about it, but we're mostly relying on manually asserting.

If you really wanted to you could do this with a Result-like discriminated union:

type Result<R,E>
  = { error: false, value: R }
  | { error: true, value: E }

function myFunction(callback: (res: Result<string, Error>) => void) {
  if (Math.random() > 0.5) {
    callback({ error: true, value: new Error('error!') });
  } else {
    callback({ error: false, value: 'ok!' })
  }
}

myFunction((res) => {
  if (res.error) {
    // type of res.value is narrowed to Error
  } else {
    // type of res.value is narrowed to string
  }
})

Which ends up being pretty nice honestly, but that's a lot of boilerplate and totally goes against common node style.

So my question is does typescript currently have a way to make this super common pattern both typesafe and convenient? I'm pretty sure the answer is no right now, and that's not a big deal, but I was just curious.

Thanks!

like image 292
Alex Guerra Avatar asked Nov 01 '25 23:11

Alex Guerra


1 Answers

The only good pattern I've seen, other than what you've done, looks like this:

function isOK<T>(err: Error | null, value: T | undefined): value is T {
    return !err;
}

declare function readdir(path: string, callback: (err: null | Error, files: string[] | undefined) => void): void;

readdir('foo', (err, files) => {
    if (isOK(err, files)) {
        files.slice(0);
    } else {
        // need to err! here but 'files' is 'undefined'
        console.log(err!.message);
    }
})
like image 95
Ryan Cavanaugh Avatar answered Nov 03 '25 14:11

Ryan Cavanaugh