Tom
Tom

Reputation: 8127

In Typescript how do you make a distinction between Node and vanilla Javascript Error types?

I have the following function:

/**
 * Retrieves a component template from filesystem
 */
const getComponentTemplate = async (
  p: string
): Promise<string> => {
  let template: string
  try {
    template = await fs.readFile(p, {
      encoding: 'utf8'
    })
  } catch (e) {
    if (e instanceof Error && e.code === 'ENOENT') {
      throw new Error(`template for element type ${elementType} not found`)
    }
    throw e
  }

  return template
}

Typescript complains here:

[ts] Property 'code' does not exist on type 'Error'

This is because the Javascript Error class only has properties message and name.

However, Node's Error class does have a code property.

Typescript defines this in a special interface ErrnoException (see source here). I have added @types/node to my package.json, but this didn't make Typescript realize that this Error is part of the ErrnoException interface.

It is not possible to declare a type annotation in a catch clause. So, how does one make the Typescript compiler able to resolve that this is a Node Error?

FYI, this is part of my tsconfig.json:

{
  "compilerOptions": {
    "target": "es2017",
    "module": "commonjs",
    "lib": ["es2017"]
    ...
  }
}

Upvotes: 17

Views: 10248

Answers (6)

user310988
user310988

Reputation:

If you want to use try/catch then you'll be getting an object you don't know the type of.

The code you already have tests to see if that object is an Error, and if it is then it casts it as a "normal" JS Error object.

You can use a typeguard to tell the type system what type the object actually is.

Something along the lines of:

function isError(error: any): error is ErrnoException { return error instanceof Error; }

I had a look at fs.readFile and it seems a common way of using that function, and indeed the entire node api, is by passing it a callback which gets called either when the job is done, or there has been an error.

And looking at the type definition it shows that the error object passed to the callback is indeed the desired ErrnoException.

export function readFile(path: PathLike | number, callback: (err: NodeJS.ErrnoException, data: Buffer) => void): void;

So using the callback will eliminate the need for the type guard, and seems to be the node way of approaching this.

This article apparently details some of the thinking behind the "callback all the things" approach.

Node’s heavy use of callbacks dates back to a style of programming older than JavaScript itself. Continuation-Passing Style (CPS) is the old-school name for how Node.js uses callbacks today. In CPS, a “continuation function” (read: “callback”) is passed as an argument to be called once the rest of that code has been run. This allows different functions to asynchronously hand control back and forth across an application.

Node.js relies on asynchronous code to stay fast, so having a dependable callback pattern is crucial. Without one, developers would be stuck maintaining different signatures and styles between each and every module. The error-first pattern was introduced into Node core to solve this very problem, and has since spread to become today’s standard. While every use-case has different requirements and responses, the error-first pattern can accommodate them all.

Upvotes: 5

Filyus
Filyus

Reputation: 107

I think the snippets below are more correct. Notice the || instead of &&.

const isNodeError = (error: any): error is NodeJS.ErrnoException => {
  if (error instanceof Error) {
    return 'errno' in error || 'code' in error || 'path' in error || 'syscall' in error;
  }
  return false;
};
const isNodeError = (error: any): error is NodeJS.ErrnoException => {
  if (error instanceof Error) {
    const nodeError: NodeJS.ErrnoException = error;
    return (
      (typeof nodeError.errno === 'number') ||
      (typeof nodeError.code === 'string') ||
      (typeof nodeError.path === 'string') ||
      (typeof nodeError.syscall === 'string')
    );
  }
  return false;
};

If you are using only the code property, you can simplify it even more:

const isNodeError = (error: any): error is NodeJS.ErrnoException => {
  return error instanceof Error && 'code' in error;
};
const isNodeError = (error: any): error is NodeJS.ErrnoException => {
  return error instanceof Error && typeof (<NodeJS.ErrnoException>error).code === 'string';
};

Upvotes: 2

Mikael Finstad
Mikael Finstad

Reputation: 872

This is not too ugly and works:

e instanceof Error && 'code' in e && e.code === 'ENOENT'

Upvotes: 5

Takeshi Tokugawa YD
Takeshi Tokugawa YD

Reputation: 923

Type-safe TypeScript solution

It is not the universal solution, but works for the ErrnoException case. According "@types/node": "16.11.xx" definitins, the ErrnoException is the interface:

interface ErrnoException extends Error {
   errno?: number | undefined;
   code?: string | undefined;
   path?: string | undefined;
   syscall?: string | undefined;
}

Below type guard fully respects this defenition. My TypeScript and ESLint settings are pretty strict, so with a high probability you will not need the comments disabling the ESLint/TSLint (if you still use this depricated one).

function isErrnoException(error: unknown): error is ErrnoException {
  return isArbitraryObject(error) &&
    error instanceof Error &&
    (typeof error.errno === "number" || typeof error.errno === "undefined") &&
    (typeof error.code === "string" || typeof error.code === "undefined") &&
    (typeof error.path === "string" || typeof error.path === "undefined") &&
    (typeof error.syscall === "string" || typeof error.syscall === "undefined");
}

where

type ArbitraryObject = { [key: string]: unknown; };

function isArbitraryObject(potentialObject: unknown): potentialObject is ArbitraryObject {
  return typeof potentialObject === "object" && potentialObject !== null;
}

Now we can check the code property:

import FileSystem from "fs";
import PromisfiedFileSystem from "fs/promises";

// ...

let targetFileStatistics: FileSystem.Stats;

try {

  targetFileStatistics = await PromisfiedFileSystem.stat(validAbsolutePathToPublicFile);

} catch (error: unknown) {

  if (isErrnoException(error) && error.code === "ENOENT") {

     response.
         writeHead(HTTP_StatusCodes.notFound, "File not found.").
         end();

     return;
  }

 
  response.
      writeHead(HTTP_StatusCodes.internalServerError, "Error occurred.").
      end();
}

I added isErrnoException type guard to my library @yamato-daiwa/es-extensions-nodejs but because I know that the promotion of third-party solutions could be annoying, I have posted the full implementation with usage example above =)

Upvotes: 12

Tom
Tom

Reputation: 8127

I ended up using @AndyJ's comment:

/**
 * Retrieves a component template from filesystem
 */
const getComponentTemplate = async (
  p: string
): Promise<string> => {
  let template: string
  try {
    template = await fs.readFile(p, {
      encoding: 'utf8'
    })
  } catch (e) {
    // tslint:disable-next-line:no-unsafe-any
    if (isNodeError(e) && e.code === 'ENOENT') {
      throw new Error(`template for element type ${elementType} not found`)
    }
    throw e
  }

  return template
}

/**
 * @param error the error object.
 * @returns if given error object is a NodeJS error.
 */
const isNodeError = (error: Error): error is NodeJS.ErrnoException =>
  error instanceof Error

But I am surprised to see that this is necessary. Also it requires you to disable tslint's unsafe-any rule if you are using that.

Upvotes: 5

HugoTeixeira
HugoTeixeira

Reputation: 4884

You may consider reading the code property using square brackets and then checking if its value equals ENOENT:

try {
    ...
} catch (e) {
    const code: string = e['code'];
    if (code === 'ENOENT') {
        ...
    }
    throw e
}

This isn't a perfect solution, but it may be good enough considering that you cannot declare types in catch clauses and that the e instanceof ErrnoException check doesn't work properly (as discussed in the question comments).

Upvotes: 2

Related Questions