Skip to content

Instantly share code, notes, and snippets.

@christophemarois
Last active January 18, 2023 18:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save christophemarois/b06bde5ff86922894f4ef18b9df3fb4c to your computer and use it in GitHub Desktop.
Save christophemarois/b06bde5ff86922894f4ef18b9df3fb4c to your computer and use it in GitHub Desktop.
Create errors with custom codes, values and messages with full type inference
export function createStructuredError<
ErrorCodes extends Record<any, (data: any) => string>
>(name: string, codes: ErrorCodes) {
return class StructuredError<
ErrorCode extends keyof ErrorCodes,
ErrorData extends Parameters<ErrorCodes[ErrorCode]>[0]
> extends Error {
constructor(
public readonly code: ErrorCode,
public readonly data: ErrorData
) {
super(codes[code](data))
Object.setPrototypeOf(this, StructuredError.prototype)
Object.defineProperty(this.constructor, 'name', { value: name })
}
static codes = codes
}
}
const FormError = createStructuredError('FormError', {
invalidEmail({ email }: { email: string }) {
return `Email ${email} is invalid`
},
invalidPasswordConfirm({ a, b }: { a: string; b: string }) {
return `Password ${a} and confirm ${b} are different`
},
})
const err = new FormError('invalidEmail', { email: 'aa@aa' })
console.error(err)
/*
FormError: Email aa@aa is invalid
(...stack)
code: 'invalidEmail',
data: { email: 'aa@aa' }
*/
export function createStructuredError<
CodeDict extends Record<any, (data: any) => string>
>(name: string, codeDict: CodeDict) {
const StructuredError = class StructuredError<
Code extends keyof CodeDict,
Data extends Parameters<CodeDict[Code]>[0]
> extends Error {
constructor(public readonly code: Code, public readonly data: Data) {
super(codeDict[code](data))
Object.setPrototypeOf(this, StructuredError.prototype)
// Will appear in the stack trace. Defined this way because typescript
// doesn't let us overwrite it as a setter
Object.defineProperty(this.constructor, 'name', { value: name })
}
/** An array containing the possible codes for this structured error */
static codes: Array<keyof CodeDict> = Object.keys(codeDict)
}
// TS doesn't support dynamic static props, so we have to do the work ourselves
// we begin by specifying explicitely the type signatures of the properties
// we're going to add
type DynamicStaticProps = {
[Code in keyof CodeDict]: (
data: Parameters<CodeDict[Code]>[0]
) => InstanceType<
typeof StructuredError<Code, Parameters<CodeDict[Code]>[0]>
>
}
// Then we patch them directly on the class constructor
for (const code of StructuredError.codes) {
const instanciator = (data: unknown) => new StructuredError(code, data)
Object.defineProperty(StructuredError, code, { value: instanciator })
}
// Finally, we return the class with a synthetic type obtained from the
// union of the real class and the patched dynamic static props
return StructuredError as typeof StructuredError & DynamicStaticProps
}
const FormError = createStructuredError('FormError', {
invalidEmail(email: string) {
return `Email ${email} is invalid`
},
invalidPasswordConfirm({ a, b }: { a: string; b: string }) {
return `Password ${a} and confirm ${b} are different`
},
})
FormError.codes // => ['invalidEmail', 'invalidPasswordConfirm']
const err1 = new FormError('invalidEmail', 'aa@aa')
console.error(err1)
/*
FormError: Email aa@aa is invalid
(...stack) {
code: 'invalidEmail',
data: { email: 'aa@aa' }
}
*/
console.assert(err1 instanceof FormError)
console.assert(err1 instanceof Error)
const err2 = FormError.invalidPasswordConfirm({ a: 'foo', b: 'bar' })
console.error(err2)
/*
FormError: Password foo and confirm bar are different
(...stack) {
code: 'invalidPasswordConfirm',
data: { a: 'foo', b: 'bar' }
}
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment