betterthrow
Version:
Define the errors your program can generate, create beautiful unions.
353 lines (352 loc) • 15.8 kB
text/typescript
import { ResultLikeSymbol, type ResultLikeUnion } from "retuple-symbols";
export type betterthrow = typeof betterthrow;
export type error = typeof error;
export type group = typeof group;
export type ErrorCodes<T extends BetterThrowInfer> = T extends BetterThrowInfer<infer TCodes, any, any> ? TCodes : never;
export type Errors<T extends BetterThrowInfer, K extends ErrorCodes<T> = ErrorCodes<T>> = T extends BetterThrowInfer<any, infer TInstances, any> ? Pick<TInstances, K>[K] : never;
export type ErrorObjects<T extends BetterThrowInfer, K extends ErrorCodes<T> = ErrorCodes<T>> = T extends BetterThrowInfer<any, any, infer TObjects> ? Pick<TObjects, K>[K] : never;
export declare function betterthrow<TKind extends string = "">(kind?: validKeyword<TKind, "class kind">): ClassBuilderPrefix<TKind extends "" ? null : TKind>;
export declare function group(): GroupBuilderContext<"">;
export declare function group<TPrefix extends string>(prefix: TPrefix): GroupBuilderContext<TPrefix>;
export declare function error<TCode extends string>(code: validKeyword<TCode, "error code">): ErrorBuilderContext<TCode, {
message: string;
}>;
export declare function error<TCode extends string, TMessage extends string>(code: validKeyword<TCode, "error code">, message: nonEmptyString<TMessage>): ErrorBuilderContext<TCode, {
message?: string;
}>;
export declare const RESERVED_WORDS: readonly ["__proto__", "prototype", "constructor", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable", "toLocaleString", "toString", "valueOf", "name", "message", "cause", "stack", "toJSON", "toPlainObject", "kind", "code"];
declare const INTERNAL: unique symbol;
declare const INFER: unique symbol;
declare abstract class BetterThrowError extends Error {
code: string;
kind: string | null;
constructor(message: string, cause?: unknown);
[ResultLikeSymbol](): ResultLikeUnion<never, this>;
toPlainObject(): PlainObject<this>;
}
type INFER = typeof INFER;
type RESERVED_WORDS = (typeof RESERVED_WORDS)[number];
type BaseContext = Record<string, any>;
type ErrorMap = Record<string, ErrorInfer>;
type ErrorDefs = readonly [ErrorDefinitions, ...ErrorDefinitions[]];
interface ErrorDefinitions<TErrMap extends ErrorMap = any> {
[INFER]: TErrMap;
[INTERNAL](this: this): readonly ErrorSettings[];
}
interface ErrorInfer<TKind extends string | null = any, TCode extends string = any, TCtx extends BaseContext = any, TData extends BaseContext = any, TMessage extends {
message?: string;
} = any, TStatic extends string = any, TStrict extends BaseContext = {}, TReadonly extends boolean = true> {
kind: TKind;
code: TCode;
context: TCtx;
data: TData;
message: TMessage;
static: TStatic;
strict: TStrict;
readonly: TReadonly;
blank: {
[K in keyof TCtx]?: never;
};
}
interface BetterThrowInfer<TCodes extends string = any, TInstances extends Record<string, BaseContext> = any, TObjects extends Record<string, BaseContext> = any> {
[INFER]: {
codes: TCodes;
instances: TInstances;
objects: TObjects;
};
}
type MessageOption<TCtx = any> = string | ((ctx: TCtx) => string);
type MessagesSetting = {
type: "function";
message: (ctx: BaseContext) => string;
} | {
type: "string";
message: string;
} | {
type: "object";
message: Record<string, MessageOption>;
};
type DataOption = BaseContext | (() => BaseContext);
type DataChain = readonly DataOption[];
type JsonFormatFn<TErrMap extends ErrorMap> = (error: BetterThrowError & InstanceProps<TErrMap[keyof TErrMap]>) => unknown;
interface ClassSettings {
kind: string | null;
prefix: string;
data: BaseContext | (() => BaseContext) | undefined;
errors: readonly ErrorDefinitions[];
messages: MessagesSetting | undefined;
json: JsonFormatFn<any> | boolean;
props: readonly string[] | undefined;
}
interface GroupSettings {
prefix: string;
errors: readonly ErrorDefinitions[];
data: BaseContext | (() => BaseContext) | undefined;
}
interface ErrorSettings {
code: string;
message: string | undefined;
chain: DataChain | undefined;
}
type ConstructContext<TErr extends ErrorInfer> = pretty<TErr["context"] & TErr["message"] & TErr["strict"] & {
cause?: unknown;
}>;
type MessageContext<TErrMap extends ErrorMap> = {
[K in keyof TErrMap]: InstanceProps<TErrMap[K]>;
}[keyof TErrMap];
type BlankContext<TErrMap extends ErrorMap> = unionToIntersection<TErrMap[keyof TErrMap]["blank"]>;
type InstanceTypes<TErrMap extends ErrorMap> = {
[K in keyof TErrMap]: BetterThrowError & InstanceProps<TErrMap[K]>;
};
type InstanceProps<TErr extends ErrorInfer> = pretty<{
kind: TErr["kind"];
code: TErr["code"];
} & mergeToData<TErr["context"], TErr["data"]>>;
type ConstructArgs<TErr extends ErrorInfer> = Partial<ConstructContext<TErr>> extends ConstructContext<TErr> ? [params?: ConstructContext<TErr>] : [params: ConstructContext<TErr>];
type StaticProps<TErrMap extends ErrorMap> = pretty<{
[TCode in keyof TErrMap & string as toPascalCase<TErrMap[TCode]["static"]>]: {
new (...args: ConstructArgs<TErrMap[TCode]>): BetterThrowError & InstanceProps<TErrMap[TCode]>;
};
} & {
readonly kind: TErrMap[keyof TErrMap]["kind"];
readonly codes: readonly (keyof TErrMap & string)[];
is: (value: unknown) => value is {
[TCode in keyof TErrMap & string]: BetterThrowError & InstanceProps<TErrMap[TCode]>;
}[keyof TErrMap & string];
} & BetterThrowInfer<keyof TErrMap & string, InstanceTypes<TErrMap>, {
[K in keyof TErrMap]: PlainObject<InstanceProps<TErrMap[K]>>;
}>>;
type PlainObject<T extends BaseContext> = pretty<Omit<T, ResultLikeSymbol | "is" | Exclude<RESERVED_WORDS, "kind" | "code" | "message">>>;
/**
* @TODO
*/
declare class ClassBuilderBuild<TErrMap extends ErrorMap> {
protected settings: ClassSettings;
constructor(settings: ClassSettings);
/**
* ## Build
*
* @TODO
*/
build({ classNames }?: {
classNames?: boolean;
}): {
new <TCode extends keyof TErrMap = keyof TErrMap>(params: {
code: TCode;
} & ConstructContext<TErrMap[TCode]>): InstanceTypes<TErrMap>[TCode];
} & StaticProps<TErrMap>;
}
/**
* @TODO
*/
declare class ClassBuilderTypes<TErrMap extends ErrorMap> extends ClassBuilderBuild<TErrMap> {
/**
* ## Types
*
* @TODO
*/
types<TInput extends "exact" | "extended", TOutput extends "exact" | "extended">(this: ClassBuilderTypes<TErrMap>, types: {
input?: TInput;
output?: TOutput;
}): ClassBuilderBuild<{
[K in keyof TErrMap]: ErrorInfer<TErrMap[K]["kind"], TErrMap[K]["code"], TErrMap[K]["context"], TOutput extends "extended" ? pretty<TErrMap[K]["data"] & Omit<BlankContext<TErrMap>, keyof TErrMap[K]["data"]>> : TErrMap[K]["data"], TErrMap[K]["message"], TErrMap[K]["static"], TInput extends "extended" ? pretty<Omit<BlankContext<TErrMap>, keyof TErrMap[K]["context"]>> : TErrMap[K]["strict"]>;
}>;
}
declare class ClassBuilderFilter<TErrMap extends ErrorMap> extends ClassBuilderTypes<TErrMap> {
/**
* ## Filter
*
* @TODO
*/
filter(this: ClassBuilderFilter<TErrMap>, filter: Record<keyof BlankContext<TErrMap>, true>): ClassBuilderTypes<TErrMap>;
}
/**
* @TODO
*/
declare class ClassBuilderJson<TErrMap extends ErrorMap> extends ClassBuilderFilter<TErrMap> {
/**
* ## JSON
*
* @TODO
*/
json(this: ClassBuilderJson<TErrMap>, enabled: boolean): ClassBuilderFilter<TErrMap>;
json(this: ClassBuilderJson<TErrMap>, formatter: JsonFormatFn<TErrMap>): ClassBuilderFilter<TErrMap>;
}
/**
* @TODO
*/
declare class ClassBuilderMessages<TErrMap extends ErrorMap> extends ClassBuilderJson<TErrMap> {
/**
* ## Messages
*
* @TODO
*/
messages<const TMsgs extends {
readonly [K in keyof TErrMap & string]?: MessageOption<Readonly<InstanceProps<TErrMap[K]>>>;
}>(this: ClassBuilderMessages<TErrMap>, message: TMsgs): ClassBuilderJson<{
[K in keyof TErrMap]: ErrorInfer<TErrMap[K]["kind"], TErrMap[K]["code"], TErrMap[K]["context"], TErrMap[K]["data"], K extends keyof TMsgs ? {
message?: string;
} : TErrMap[K]["message"], TErrMap[K]["static"]>;
}>;
messages(this: ClassBuilderMessages<TErrMap>, messages: (ctx: Readonly<MessageContext<TErrMap>>) => string): ClassBuilderJson<{
[K in keyof TErrMap]: ErrorInfer<TErrMap[K]["kind"], TErrMap[K]["code"], TErrMap[K]["context"], TErrMap[K]["data"], {
message?: string;
}, TErrMap[K]["static"]>;
}>;
messages<TMessage extends string>(this: ClassBuilderMessages<TErrMap>, messages: nonEmptyString<TMessage>): ClassBuilderJson<{
[K in keyof TErrMap]: ErrorInfer<TErrMap[K]["kind"], TErrMap[K]["code"], TErrMap[K]["context"], TErrMap[K]["data"], {
message?: string;
}, TErrMap[K]["static"]>;
}>;
private getMessagesSetting;
}
/**
* @TODO
*/
declare class ClassBuilderErrors<TKind extends string | null, TPrefix extends string, TCtx extends BaseContext, TData extends BaseContext> {
protected settings: ClassSettings;
constructor(settings: ClassSettings);
/**
* ## Errors
*
* @TODO
*/
errors<TErrs extends ErrorDefs>(...errors: TErrs): ClassBuilderMessages<{
[K in keyof toErrMap<TErrs> & string as toPrefixed<TPrefix, K>]: ErrorInfer<TKind, toPrefixed<TPrefix, K>, pretty<mergeToContext<overwrite<TCtx, toErrMap<TErrs>[K]["context"]>, overwrite<TData, toErrMap<TErrs>[K]["data"]>>>, pretty<overwrite<TData, toErrMap<TErrs>[K]["data"]>>, toErrMap<TErrs>[K]["message"], toErrMap<TErrs>[K]["static"]>;
}>;
}
/**
* @TODO
*/
declare class ClassBuilderData<TKind extends string | null, TPrefix extends string, TCtx extends BaseContext> extends ClassBuilderErrors<TKind, TPrefix, TCtx, {}> {
/**
* ## Data
*
* @TODO
*/
data<const TData extends BaseContext>(data: (TData & validObjectKeys<TData, "class", "data">) | (() => TData)): ClassBuilderErrors<TKind, TPrefix, mergeToContext<TCtx, validObjectKeys<TData, "class", "data">>, validObjectKeys<TData, "class", "data">>;
}
/**
* @TODO
*/
declare class ClassBuilderContext<TKind extends string | null, TPrefix extends string> extends ClassBuilderData<TKind, TPrefix, {}> {
/**
* ## Context
*
* @TODO
*/
context<TCtx extends BaseContext = {}>(): ClassBuilderData<TKind, TPrefix, validObjectKeys<TCtx, "class", "context">>;
}
/**
* @TODO
*/
declare class ClassBuilderPrefix<TKind extends string | null> extends ClassBuilderContext<TKind, ""> {
/**
* ## Prefix
*
* @TODO
*/
prefix<TPrefix extends string>(prefix: TPrefix): ClassBuilderContext<TKind, TPrefix>;
}
/**
* ## Group Definition
*
* @TODO
*/
declare class GroupDefinition<TErrMap extends ErrorMap> implements ErrorDefinitions<TErrMap> {
protected settings: GroupSettings;
[INFER]: TErrMap;
constructor(settings: GroupSettings);
[INTERNAL](): readonly ErrorSettings[];
}
/**
* @TODO
*/
declare class GroupBuilderErrors<TPrefix extends string, TCtx extends BaseContext, TData extends BaseContext> {
protected settings: GroupSettings;
constructor(settings: GroupSettings);
/**
* ## Errors
*
* @TODO
*/
errors<TErrs extends ErrorDefs>(...errors: TErrs): GroupDefinition<{
[K in keyof toErrMap<TErrs> & string as toPrefixed<TPrefix, K>]: ErrorInfer<never, toPrefixed<TPrefix, K>, pretty<mergeToContext<overwrite<TCtx, toErrMap<TErrs>[K]["context"]>, overwrite<TData, toErrMap<TErrs>[K]["data"]>>>, pretty<overwrite<TData, toErrMap<TErrs>[K]["data"]>>, toErrMap<TErrs>[K]["message"], toPrefixed<TPrefix, K>>;
}>;
}
/**
* @TODO
*/
declare class GroupBuilderData<TPrefix extends string, TCtx extends BaseContext> extends GroupBuilderErrors<TPrefix, TCtx, {}> {
/**
* ## Data
*
* @TODO
*/
data<const TData extends BaseContext>(data: (TData & validObjectKeys<TData, "group", "data", TPrefix>) | (() => TData)): GroupBuilderErrors<TPrefix, mergeToContext<TCtx, validObjectKeys<TData, "group", "data", TPrefix>>, validObjectKeys<TData, "group", "data", TPrefix>>;
}
declare class GroupBuilderContext<TPrefix extends string> extends GroupBuilderData<TPrefix, {}> {
/**
* ## Context
*
* @TODO
*/
context<TCtx extends BaseContext = {}>(): GroupBuilderData<TPrefix, validObjectKeys<TCtx, "group", "context", TPrefix>>;
}
/**
* ## Error Definition
*
* @TODO
*/
declare class ErrorDefinition<TErrMap extends ErrorMap> implements ErrorDefinitions<TErrMap> {
protected settings: ErrorSettings;
[INFER]: TErrMap;
constructor(settings: ErrorSettings);
[INTERNAL](): [ErrorSettings];
}
declare class ErrorBuilderData<TCode extends string, TCtx extends BaseContext, TMessage extends {
message?: string;
}> extends ErrorDefinition<{
[K in TCode]: ErrorInfer<never, TCode, TCtx, {} /** @TODO data */, TMessage, TCode>;
}> {
/**
* ## Data
*
* @TODO
*/
data<const TData extends BaseContext>(data: (TData & validObjectKeys<TData, "error", "data", TCode>) | (() => TData)): ErrorDefinition<{
[K in TCode]: ErrorInfer<never, TCode, mergeToContext<TCtx, validObjectKeys<TData, "error", "data", TCode>>, validObjectKeys<TData, "error", "data", TCode>, TMessage, TCode>;
}>;
}
declare class ErrorBuilderContext<TCode extends string, TMessage extends {
message?: string;
}> extends ErrorBuilderData<TCode, {}, TMessage> {
/**
* ## Context
*
* @TODO
*/
context<TCtx extends BaseContext = {}>(): ErrorBuilderData<TCode, validObjectKeys<TCtx, "error", "context", TCode>, TMessage>;
}
declare function toPascalCase(code: string): string;
type toErrMap<TErrs extends ErrorDefs> = unionToIntersection<TErrs[number][INFER]>;
type mergeToContext<TCtx extends BaseContext, TData extends BaseContext> = {
[K in keyof TCtx & keyof TData]?: TCtx[K];
} & Omit<TCtx, keyof TData>;
type mergeToData<TCtx extends BaseContext, TData extends BaseContext> = {
[K in keyof TCtx & keyof TData]: Exclude<TCtx[K], undefined> | TData[K];
} & Omit<TCtx, keyof TData> & Omit<TData, keyof TCtx>;
type toPrefixed<TPrefix extends string, TCode extends string> = TPrefix extends "" ? TCode : `${TPrefix}_${TCode}`;
type toPascalCase<T extends string> = T extends `${infer TWord}_${infer Rest}` ? `${Capitalize<Lowercase<TWord>>}${toPascalCase<Rest>}` : Capitalize<Lowercase<T>>;
type validKeyword<T extends string, MTarget extends "class kind" | "class prefix" | "group prefix" | "error code"> = T extends `${string} ${string}` ? `BetterThrow: ${Capitalize<MTarget>} '${T}' must not contain spaces.` : nonEmptyString<T>;
type nonEmptyString<T extends string> = T extends "" ? never : T;
type validObjectKeys<TCtx extends BaseContext, MTarget extends "class" | "group" | "error", MObject extends "context" | "data", MName extends string = ""> = pretty<{
[K in keyof TCtx as K extends RESERVED_WORDS ? `BetterThrow: '${K}' is reserved and can't be used as a ${MObject} property (on ${MTarget}${getNameMessage<MName>}).` : K extends symbol ? `BetterThrow: Symbols can't be used as a ${MObject} property (on ${MTarget}${getNameMessage<MName>}).` : K]: K extends RESERVED_WORDS | symbol ? never : TCtx[K];
}>;
type getNameMessage<MName extends string> = MName extends "" ? "" : ` '${MName}'`;
type pretty<T> = {
-readonly [K in keyof T]: T[K];
} & {};
type overwrite<TBase extends BaseContext, TExt extends BaseContext> = Omit<TBase, keyof TExt> & TExt;
type unionToIntersection<TUnion> = (TUnion extends any ? (union: TUnion) => any : never) extends (intersection: infer TIntersection extends Record<string, any>) => any ? TIntersection : never;
export {};