Is it feasible to create type-safe callbacks in the style of Node.js?

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!

Answer №1

I have come across a unique pattern that stands out from the others, apart from what you have already implemented:

function isValid<T>(error: Error | null, data: T | undefined): data is T {
    return !error;
}

declare function readDirectory(path: string, callback: (error: null | Error, fileList: string[] | undefined) => void): void;

readDirectory('foo', (error, fileList) => {
    if (isValid(error, fileList)) {
        fileList.slice(0);
    } else {
        // An error needs to be handled here, but 'fileList' is 'undefined'
        console.log(error!.message);
    }
})

Answer №2

A recent development in TypeScript 4.6 introduces a new feature called Control Flow Analysis for Dependent Parameters, which now allows for this functionality.

Take a look at the specific type definition for readdir:

type readdir =
  ( path: string
  , cb: (...args: [NodeJS.ErrnoException, undefined] | [null, string[]]) => void
  ) => void
const myreaddir: readdir = fs.readdir as any

This code snippet addresses the issue you had previously mentioned:

myreaddir('/', (err, files) => {
  if (err !== null) {
    console.log(files.length)
             // └──── 18048: 'files' is possibly 'undefined'.
  }
})

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

What is the proper method for utilizing a conditional header in an rtk query?

How can I implement conditional header authentication using rtk query? I need to pass headers for all requests except refresh token, where headers should be empty. Is it possible to achieve this by setting a condition or using two separate fetchBaseQuery ...

Attempt to create a truncated text that spans two lines, with the truncation occurring at the beginning of the text

How can I truncate text on two lines with truncation at the beginning of the text? I want it to appear like this: ... to long for this div I haven't been able to find a solution. Does anyone have any suggestions? Thanks in advance! ...

I am looking to transfer 'beforeEach' and 'afterEach' from the spec file to the global configuration file in WDIO [mocha, hook, wdio]

My E2E testing setup involves using the WebdriverIO library along with the mocha framework. During test execution, I want Mocha to automatically skip all subsequent checks in a test after encountering the first error, and proceed to the next test file. T ...

The subsequent code still running even with the implementation of async/await

I'm currently facing an issue with a function that needs to resolve a promise before moving on to the next lines of code. Here is what I expect: START promise resolved line1 line2 line3 etc ... However, the problem I'm encountering is that all t ...

The Typescript decorator is unable to access the property type within its own scope

I am currently in the process of developing a dependency injector for use in my VUE js project. Recently, I created an Inject decorator with the intention of accessing a property type. It was functioning perfectly fine yesterday, but now it seems that som ...

Obtain the count of unique key-value pairs represented in an object

I received this response from the server: https://i.stack.imgur.com/TvpTP.png My goal is to obtain the unique key along with its occurrence count in the following format: 0:{"name":"physics 1","count":2} 1:{"name":"chem 1","count":6} I have already rev ...

Angular2 Bootstrap modal problem: Troubleshooting the issue

I'm currently working on an angular2 project, and we are utilizing the package "bootstrap": "~3.3.7". We have encountered an issue while attempting to open the modal. Upon inspecting the HTML, we found the following code: <ngb-modal-backdrop clas ...

I am facing an issue with the asynchronous function as it is displaying an error message

**I am facing an issue with displaying categories. I have attempted to do this using async function, however the data is not showing up** <div class="form-group"> <label for="category">Category</label> <select id="categor ...

How can I properly type my object using generics if the Typescript code `let result: { [key: T[K]]: T } = {};` is not functioning as expected?

I am developing a function called keyBy that has a simple purpose. The objective of this function is to transform an array of objects into an object, using a provided 'key string': const arr = [{ name: 'Jason', age: 18 }, { name: &apo ...

Ways to maximize your final export value

My data file, named data.ts, contains a large dataset: export data = [ ... // huge data ] The lib.ts file only utilizes a portion of the data: import { data } from './data.ts'; const fitteredData = data.splice(0,2) // only use some of them ...

`Troubleshooting problem with debugging mocha tests in a TypeScript environment`

I'm currently facing an issue while trying to debug a mocha test. Despite my efforts in searching on Google and Stack Overflow, I have not been able to find a solution. The error message is as follows: TSError: ⨯ Unable to compile TypeScript: sour ...

The type '{ }' does not include the properties 'params', 'isExact', 'path', 'url' from the 'match<Identifiable>' type

Currently, I am utilizing react router and typescript in order to extract the id variable from a route for use in a component. However, typescript is raising an issue: The type '{}' lacks the following properties found in type 'match' ...

Implementing an external abstract class in Typescript without inheriting from it

In my code, there is a module named utils.tsx defined as follows: interface IUtils { toUri: (route: string) => string } export default abstract class Utils implements IUtils { public toUri = (route: string) => { return route } } ...

Enhance the functionality of angular-material buttons by incorporating dynamic loading animations into

I am currently working on a solution in Angular 12 to disable a button (and show a spinner) when it is clicked, until the API responds. To achieve this, I plan to follow a similar approach to the angular-material button implementation. Essentially, I want ...

The submission functionality of an Angular form can be triggered by a separate button

I am currently developing a Material App using Angular 12. The Form structure I have implemented is as follows: <form [formGroup]="form" class="normal-form" (ngSubmit)="onSubmit()"> <mat-grid-list cols="2" ...

What are the steps to effectively troubleshoot TypeScript code in Visual Studio 2017?

Currently working on an ASP.NET Core project that heavily utilizes TypeScript. Does Visual Studio support debugging TypeScript code? ...

Utilize nested object models as parameters in TypeScript requests

Trying to pass request parameters using model structure in typescript. It works fine for non-nested objects, but encountering issues with nested arrays as shown below: export class exampleModel{ products: [ { name: string, ...

Adjusting the interface of a third-party TypeScript library

I am currently working on modifying a third-party interface. I'm curious about why this particular code is successful: import { LoadableComponentMethods as OldLoadableComponentMethods } from '@loadable/component'; declare module "load ...

The argument provided does not match the expected parameters

import { Component } from '@angular/core'; import { IonicPage, NavController, NavParams } from 'ionic-angular'; import { EventDetail } from '../../models/event-detail/event-detail.interface'; import { AngularFireDatabase, Angu ...

Issues arise when trying to type ChangeEvent in React using Typescript

After spending some time learning React with TypeScript, I encountered a problem. The prop onChangeHandler in my code takes a function to modify properties in formik values. <Formik<FormModel> initialValues={{ favorite: ...