error-serializer
Version:
Convert errors to/from plain objects
404 lines (381 loc) • 11.4 kB
TypeScript
/**
* Any JSON value.
*/
type JSONValue =
| null
| boolean
| number
| string
| JSONValue[]
| { [key: string]: JSONValue }
interface MinimalErrorObject {
message: string
[key: PropertyKey]: JSONValue
}
/**
* Error instance converted to a plain object
*/
export interface ErrorObject extends MinimalErrorObject {
name: string
stack: string
cause?: ErrorObject
errors?: ErrorObject[]
}
/**
* `error-serializer` `serialize()` options
*/
export interface SerializeOptions {
/**
* Unless this option is `true`, nested errors are also serialized.
* They can be inside other errors, plain objects or arrays.
*
* @default false
*
* @example
* ```js
* const error = new Error('example')
* error.inner = new Error('inner')
* serialize(error).inner // { name: 'Error', message: 'inner', ... }
* serialize(error, { shallow: true }).inner // Error: inner ...
* ```
*/
readonly shallow?: boolean
/**
* By default, when the argument is not an `Error` instance, it is converted
* to one. If this option is `true`, it is kept as is instead.
*
* @default false
*
* @example
* ```js
* serialize('example') // { name: 'Error', message: 'example', ... }
* serialize('example', { loose: true }) // 'example'
* ```
*/
readonly loose?: boolean
/**
* Only pick specific properties.
*
* @example
* ```js
* serialize(error, { include: ['message'] }) // { message: 'example' }
* ```
*
* @example
* ```js
* const error = new Error('example')
* error.prop = true
*
* const errorObject = serialize(error, { include: ['name', 'message', 'stack'] })
* console.log(errorObject.prop) // undefined
* console.log(errorObject) // { name: 'Error', message: 'example', stack: '...' }
* ```
*/
readonly include?: readonly string[]
/**
* Omit specific properties.
*
* @example
* ```js
* serialize(error, { exclude: ['stack'] }) // { name: 'Error', message: 'example' }
* ```
*
* @example
* ```js
* const error = new Error('example')
*
* const errorObject = serialize(error, { exclude: ['stack'] })
* console.log(errorObject.stack) // undefined
* console.log(errorObject) // { name: 'Error', message: 'example' }
* ```
*/
readonly exclude?: readonly string[]
/**
* Transform each error plain object.
*
* `errorObject` is the error after serialization. It must be directly
* mutated.
*
* `errorInstance` is the error before serialization.
*
* @example
* ```js
* const errors = [new Error('test secret')]
* errors[0].date = new Date()
*
* const errorObjects = serialize(errors, {
* loose: true,
* // Serialize `Date` instances as strings
* transformObject: (errorObject) => {
* errorObject.date = errorObject.date.toString()
* },
* })
* console.log(errorObjects[0].date) // Date string
*
* const newErrors = parse(errorObjects, {
* loose: true,
* // Transform error message
* transformArgs: (constructorArgs) => {
* constructorArgs[0] = constructorArgs[0].replace('secret', '***')
* },
* // Parse date strings as `Date` instances
* transformInstance: (error) => {
* error.date = new Date(error.date)
* },
* })
* console.log(newErrors[0].message) // 'test ***'
* console.log(newErrors[0].date) // `Date` instance
* ```
*/
readonly transformObject?: (
errorObject: ErrorObject,
errorInstance: Error,
) => void
}
/**
* Convert an `Error` instance into a plain object.
*
* @example
* ```js
* const error = new TypeError('example')
* const errorObject = serialize(error)
* // Plain object: { name: 'TypeError', message: 'example', stack: '...' }
*
* const errorString = JSON.stringify(errorObject)
* const newErrorObject = JSON.parse(errorString)
*
* const newError = parse(newErrorObject)
* // Error instance: 'TypeError: example ...'
* ```
*/
export function serialize<Value, Options extends SerializeOptions = object>(
errorInstance: Value,
options?: Options,
): Options['shallow'] extends true
? SerializeShallow<SerializeNormalize<Value, Options>>
: SerializeDeep<SerializeNormalize<Value, Options>>
type SerializeNormalize<
Value,
Options extends SerializeOptions,
> = Options['loose'] extends true ? Value : Value extends Error ? Value : Error
type SerializeShallow<Value> = Value extends Error
? ErrorObject & {
[Key in keyof Value as Value[Key] extends ErrorObject[Key]
? Key
: never]: Value[Key]
}
: Value
type SerializeDeep<Value> = Value extends Error
? ErrorObject & {
[Key in keyof Value as SerializeDeep<Value[Key]> extends ErrorObject[Key]
? Key
: never]: SerializeDeep<Value[Key]>
}
: Value extends object
? { [Key in keyof Value]: SerializeDeep<Value[Key]> }
: Value
// `Error` is both a `CallableFunction` and a `NewableFunction`, which makes
// `typeof Error` not work as expected.
type ErrorClass = new (message: string) => Error
/**
* `error-serializer` `parse()` options
*/
export interface ParseOptions {
/**
* Unless this option is `true`, nested error plain objects are also parsed.
* They can be inside other errors, plain objects or arrays.
*
* @default false
*
* @example
* ```js
* const error = new Error('example')
* error.inner = new Error('inner')
* const errorObject = serialize(error)
*
* parse(errorObject).inner // Error: inner ...
* parse(errorObject, { shallow: true }).inner // { name: 'Error', message: ... }
* ```
*/
readonly shallow?: boolean
/**
* By default, when the argument is not an error plain object, it is
* converted to one. If this option is `true`, it is kept as is instead.
*
* @default false
*
* @example
* ```js
* parse('example') // Error: example
* parse('example', { loose: true }) // 'example'
* ```
*/
readonly loose?: boolean
/**
* Custom error classes to keep when parsing.
*
* - Each key is an `errorObject.name`
* - Each value is the error class to use
*
* @example
* ```js
* const errorObject = serialize(new CustomError('example'))
* // `CustomError` class is kept
* const error = parse(errorObject, { classes: { CustomError } })
* // Map `CustomError` to another class
* const otherError = parse(errorObject, { classes: { CustomError: TypeError } })
* ```
*/
readonly classes?: { [ErrorClassName: string]: ErrorClass }
/**
* Transform the arguments passed to each `new Error()`.
*
* `constructorArgs` is the array of arguments. Usually, `constructorArgs[0]`
* is the
* [error message](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/message)
* and `constructorArgs[1]` is the
* [constructor options object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/Error#parameters).
* `constructorArgs` must be directly mutated.
*
* `errorObject` is the error before parsing.
*
* @example
* ```js
* const errors = [new Error('test secret')]
* errors[0].date = new Date()
*
* const errorObjects = serialize(errors, {
* loose: true,
* // Serialize `Date` instances as strings
* transformObject: (errorObject) => {
* errorObject.date = errorObject.date.toString()
* },
* })
* console.log(errorObjects[0].date) // Date string
*
* const newErrors = parse(errorObjects, {
* loose: true,
* // Transform error message
* transformArgs: (constructorArgs) => {
* constructorArgs[0] = constructorArgs[0].replace('secret', '***')
* },
* // Parse date strings as `Date` instances
* transformInstance: (error) => {
* error.date = new Date(error.date)
* },
* })
* console.log(newErrors[0].message) // 'test ***'
* console.log(newErrors[0].date) // `Date` instance
* ```
*/
readonly transformArgs?: (
constructorArgs: unknown[],
errorObject: ErrorObject,
ErrorClass: ErrorClass,
) => void
/**
* Transform each `Error` instance.
*
* `errorInstance` is the error after parsing. It must be directly mutated.
*
* `errorObject` is the error before parsing.
*
* @example
* ```js
* const errors = [new Error('test secret')]
* errors[0].date = new Date()
*
* const errorObjects = serialize(errors, {
* loose: true,
* // Serialize `Date` instances as strings
* transformObject: (errorObject) => {
* errorObject.date = errorObject.date.toString()
* },
* })
* console.log(errorObjects[0].date) // Date string
*
* const newErrors = parse(errorObjects, {
* loose: true,
* // Transform error message
* transformArgs: (constructorArgs) => {
* constructorArgs[0] = constructorArgs[0].replace('secret', '***')
* },
* // Parse date strings as `Date` instances
* transformInstance: (error) => {
* error.date = new Date(error.date)
* },
* })
* console.log(newErrors[0].message) // 'test ***'
* console.log(newErrors[0].date) // `Date` instance
* ```
*/
readonly transformInstance?: (
errorInstance: Error,
errorObject: ErrorObject,
) => void
}
/**
* Convert an error plain object into an `Error` instance.
*
* @example
* ```js
* const error = new TypeError('example')
* const errorObject = serialize(error)
* // Plain object: { name: 'TypeError', message: 'example', stack: '...' }
*
* const errorString = JSON.stringify(errorObject)
* const newErrorObject = JSON.parse(errorString)
*
* const newError = parse(newErrorObject)
* // Error instance: 'TypeError: example ...'
* ```
*/
export function parse<Value, Options extends ParseOptions = object>(
errorObject: Value,
options?: Options,
): Options['shallow'] extends true
? ParseNormalize<ParseShallow<Value, Options>, Options>
: ParseNormalize<ParseDeep<Value, Options>, Options>
type ParseNormalize<
Value,
Options extends ParseOptions,
> = Options['loose'] extends true ? Value : Value extends Error ? Value : Error
type ParseShallow<
Value,
Options extends ParseOptions,
> = Value extends MinimalErrorObject
? ParsedError<Value, Options> & {
[Key in keyof Value as Value[Key] extends (
Key extends keyof ParsedError<Value, Options>
? ParsedError<Value, Options>[Key]
: unknown
)
? Key
: never]: Value[Key]
}
: Value
type ParseDeep<
Value,
Options extends ParseOptions,
> = Value extends MinimalErrorObject
? ParsedError<Value, Options> & {
[Key in keyof Value as Key extends keyof ParsedError<Value, Options>
? ParseDeep<Value[Key], Options> extends ParsedError<
Value,
Options
>[Key]
? Key
: never
: Key]: ParseDeep<Value[Key], Options>
}
: Value extends object
? { [Key in keyof Value]: ParseDeep<Value[Key], Options> }
: Value
type ParsedError<
ErrorObjectArg extends MinimalErrorObject,
Options extends ParseOptions,
> = ErrorObjectArg extends { name: string }
? NonNullable<Options['classes']>[ErrorObjectArg['name']] extends ErrorClass
? InstanceType<NonNullable<Options['classes']>[ErrorObjectArg['name']]>
: Error
: Error