Node callbacks typically have a structure like this:
interface NodeCallback<TResult,TError> {
(err: TError): void;
(err: null, res: TResult): void;
}
With the callback receiving either an err
or res
, but not both. Many type definitions I've encountered hard code the types of err
and res
as non-optional.
function readdir(path: string, callback?: (err: NodeJS.ErrnoException, files: string[]) => void): void;
This setup isn't completely type-safe. For instance, the following code compiles without errors:
fs.readdir('/', (err, files) => {
if (err !== null) { // Error is present!
files.forEach(log); // Continues to use the result without any issues.
}
})
To increase safety slightly, you can modify the signature to encompass all possible values:
function readdir(path: string, callback?: (err: null | NodeJS.ErrnoException, files?: string[]) => void): void;
However, there's no direct way to specify the relationship between the two, requiring type assertion on res
to satisfy strictNullChecks
.
fs.readdir('/', (err, files) => {
if (err === null) { // No error present
// files.forEach(log); // Will not compile
(files as string[]).forEach(log); // Type assertion
files!.forEach(log); // Convenient shorthand
if (files !== undefined) { // Type guard
files.forEach(log);
}
}
})
This approach has its drawbacks:
- Repetitive implementation.
- Type assertion required even if not accessing a property, potentially needing importation of another type.
- Lack of complete safety, relying heavily on manual assertion.
If desired, a more robust solution involves using a discriminated union similar to a Result
:
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) {
// Narrowed type of res.value is now Error
} else {
// Narrowed type of res.value is now string
}
})
This method offers improved clarity but entails additional boilerplate and deviates from common node style.
Hence, the question arises: Does TypeScript offer a seamless way to enhance the typeness and ease of this prevalent pattern? As it stands, the answer leans towards no, which is acceptable but worth exploring further.
Thank you!