Alex Guerra Alex Guerra - 2 months ago 25
TypeScript Question

Is it possible to make typesafe node-style callbacks?

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!

Answer

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);
    }
})