Created
July 22, 2022 19:07
-
-
Save AprilArcus/87c17aea936b6ea292db3784ed89823a to your computer and use it in GitHub Desktop.
Pedantic JavaScript / TypeScript Error subclass
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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