Skip to content

Instantly share code, notes, and snippets.

@AprilArcus
Created July 22, 2022 19:07
Show Gist options
  • Save AprilArcus/87c17aea936b6ea292db3784ed89823a to your computer and use it in GitHub Desktop.
Save AprilArcus/87c17aea936b6ea292db3784ed89823a to your computer and use it in GitHub Desktop.
Pedantic JavaScript / TypeScript Error subclass
// Recipe for a pedantically correct subclass of JavaScript's built-in
// "Error" constructor.
// * Preserves the ability of JS Errors to be constructed with
// a `new`-less function call
// * Can be subclassed itself
// * Backward- and Forward-compatible TypeScript annotations
// * Correct enumerable / writable / configurable settings on
// .prototype, .prototype.constructor, .prototype.name, and
// custom instance fields.
// Defensive type-level programming to guard against additional
// parameters being added to Error, as happened in lib.es2022.error.d.ts
// (which introduced the second-position errorOptions argument)
type AugmentErrorOptions<
ErrorParameters extends unknown[],
ExtraOptions
> =
ErrorParameters extends [(infer Message)?]
? [Message?, ExtraOptions?]
: ErrorParameters extends [(infer Message)?, (infer ErrorOptions)?]
? [Message?, (ErrorOptions & ExtraOptions)?]
: ErrorParameters extends [
(infer Message)?,
(infer ErrorOptions)?,
...infer Rest
]
? [Message?, (ErrorOptions & ExtraOptions)?, ...Rest]
: never;
interface MyErrorOptions {
field?: string
};
interface MyError extends Error {
field: string
};
interface MyErrorConstructor {
new (
...args: AugmentErrorOptions<
ConstructorParameters<ErrorConstructor>,
MyErrorOptions
>
): MyError;
(
...args: AugmentErrorOptions<
Parameters<ErrorConstructor>,
MyErrorOptions
>
): MyError;
};
// the #__PURE__ annotation allows bundlers to tree-shake this
// IIFE into oblivion if its exported return value is not used.
export const MyError: MyErrorConstructor = /*#__PURE__*/ (() => {
'use strict';
const MyErrorConstructor = function MyError(
this: MyError | undefined,
...[
message,
{ field = 'default', ...errorOptions } = {},
...rest
]: (
& AugmentErrorOptions<
ConstructorParameters<ErrorConstructor>,
MyErrorOptions
>
& AugmentErrorOptions<
Parameters<ErrorConstructor>,
MyErrorOptions
>
)
) {
// The `super()` call. At present, Error() and new Error() are
// indistinguishable, but use whatever we were invoked with in case
// that ever changes.
const error = this
? new Error(
message,
// we have to suppress a type error here if TypeScript
// doesn't know es2022's two-argument Error constructor
// @ts-ignore-error
errorOptions,
...rest
)
: Error(
message,
// @ts-ignore-error
errorOptions,
...rest
);
// Object.setPrototypeOf() is necessary when extending built-ins,
// since Error.call(this, message, errorOptions, ...rest) doesn't
// set up the prototype chain the way it would with a user-defined
// class.
Object.setPrototypeOf(
error,
this ? Object.getPrototypeOf(this) : MyError.prototype
);
// imitates the non-enumerability of the 'message' and 'stack'
// instance properties on built-in Error. This may not be desirable
// for your custom error.
Object.defineProperties(
error,
{
field: {
value: field,
writable: true,
configurable: true
}
}
);
return error;
} as MyErrorConstructor;
// MyError.prototype, MyError.prototype.constructor, and
// MyError.prototoype.name all have to be non-enumerable to match
// the built-in RangeError, TypeError, etc., so we use
// Object.defineProperty to set them instead of `=` assignment.
// Default values for property descriptor objects:
// writable: false
// enumerable: false
// configurable: false
Object.defineProperty(
MyErrorConstructor,
'prototype',
// It's a weird wart that the built-in Error constructor
// prototypes have writable and configurable constructor and name
// fields. We follow that behavior to be consistent, not because
// it makes sense.
{
value: Object.create(
Error.prototype,
{
constructor: {
value: MyErrorConstructor,
writable: true,
configurable: true
},
// Don't forget `name`!
// h/t https://wbinnssmith.com/blog/subclassing-error-in-modern-javascript/
name: {
value: 'MyError',
writable: true,
configurable: true
}
}
),
// SomeConstructorFunction.prototype starts off writable with
// `function`-type constructors, in contrast to `class SomeClass`.
// Set this to be non-writable to match `class`es and the built-in
// Error constructors.
writable: false
}
);
return MyErrorConstructor
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment