Last active
January 18, 2023 18:16
-
-
Save christophemarois/b06bde5ff86922894f4ef18b9df3fb4c to your computer and use it in GitHub Desktop.
Create errors with custom codes, values and messages with full type inference
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
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' } | |
*/ |
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
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