Skip to content

error-c is constant based error define micro framework, you can define error with message(dynamic) more staticaly and safely on typescript

License

Notifications You must be signed in to change notification settings

egoavara/error-c

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

22 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

error-c

What is this package do?

error-c is constant based error define micro framework, you can define error with message(dynamic) more staticaly and safely on typescript.

The goal of packages is similar to that of typeScript.

When your using typescript, perhaps you want to generate SAFER code through static type analysis.

However, think about this

const DEFINE_ERRORS = {
  E0001: "This is error!",
  E0002: "I'm error too!",
  E0003: "I'm ${whoareyou}!",
  E0004: "I'm not ${notyou}, i'm ${whoareyou}!",
};

The goal of the definition of this code is clear.

You want to use error with dynamicaly generated message

However, there is no way to know what value is needed for the message of that particular error code.

Until now, not now.

Thanks for typescript infer, now we can guess what values are needed for errors

If you're not using typescript, you don't need to use this package at all.

Look at this, it's also in the examples folder.

import errorc from "error-c";

const DEFINE_ERRORS = {
    E0001: "This is error!",
    E0002: "I'm error too!",
    E0003: "I'm ${whoareyou}!",
    E0004: "I'm not ${notyou}, i'm ${whoareyou}!",
} as const;

const fail = errorc(
    "namespace",
    DEFINE_ERRORS,
    {
        defaultMode: "release",
    }
);

console.log(fail.E0004({ notyou: "pizza", whoareyou: "potato" }));

That code is simply a generate error message function that create by automated generator.

It's ok, but the generator is very powerful than your thought

Imagine that you accidentally wrote a value into the "noryou" field instead of the "notyou" field.

This is a common mistake in practice.

the compiler tells you that there is an error in the noryou part

This code throws a compile error.

Or, you don't know which fields are in a particular error code.

Now you can, see below

autocomplete tells me that struct has "notyou", "whoareyou" fields

This magic make by typescript feature, no additional plugins, no automated build scripts.

All you need to do now is modify DEFINE_ERRORS. Typescript takes care of the rest.

How to use?


Define

First of all, to use this library, you need to define it.

You can define it like the code below.

import { Define } from "error-c";

const DEFINE_ERRORS = {
  SIMPLE0: "This is error!",
  SIMPLE1: "I'm not ${notyou}, i'm ${whoareyou}!",
  // You can define nested object
  INNER : {
    HELLO : "Inner hello?",
    RENNI :{
      HELLO : "Inner.Inner hello?",
    }
  }
} as const;

WARNING

as const is VERY IMPORTANT never take it off

Importantly, DO NOT explicitly define types.


Generate

After writing the definition, the next thing to do is create an error generating function.

You can do this simply as below.

import errorc, {FunctionGenerator, NamespaceGenerate} from "error-c";

const DEFINE_ERRORS = {
    E0001: "Hello, world!",
    E0002: "I'm ${whoareyou}!",
    INNER: {
        INNER_ERROR: "inner error"
    },
} as const;

const fnStyle = errorc("function", DEFINE_ERRORS);
// or
// const fnStyle = FunctionGenerator(DEFINE_ERRORS);

const nsStyle = errorc("namespace", DEFINE_ERRORS);
// or
// const nsStyle = errorc(DEFINE_ERRORS);

// or
// const nsStyle = NamespaceGenerate(DEFINE_ERRORS);

// > Hello, world!
console.log(fnStyle("E0001", {}))
console.log(nsStyle.E0001())

// > I'm potato!
console.log(fnStyle("E0002", {whoareyou : "potato"}))
console.log(nsStyle.E0002({whoareyou : "potato"}))

// > inner error
console.log(fnStyle("INNER.INNER_ERROR", {}))
console.log(nsStyle.INNER.INNER_ERROR())

As you can see, there is two generator, function and namespace

Two are similar

But the namespace has the advantage to omitting it if there is no parameters

Also, IDEs such as vscode support shortcuts to declarations, namespace is recommended to use the namespace method whenever possible.


Conditional message

import errorc from "error-c"

const DEFINE_ERRORS = {
    CONDMSG: { release: "hello, release", debug: "hello, debug" }
} as const;

const noDefault = errorc("namespace", DEFINE_ERRORS);
const defaultIsDebug = errorc("namespace", DEFINE_ERRORS, { defaultMode: "debug", });
const defaultIsRelease = errorc("namespace", DEFINE_ERRORS, { defaultMode: "release", });

console.log(noDefault.CONDMSG());                   // expected : 'hello, release'
console.log(noDefault.CONDMSG("debug"));            // expected : 'hello, debug'
console.log(noDefault.CONDMSG("release"));          // expected : 'hello, release'
console.log(defaultIsDebug.CONDMSG());              // expected : 'hello, debug'
console.log(defaultIsDebug.CONDMSG("debug"));       // expected : 'hello, debug'
console.log(defaultIsDebug.CONDMSG("release"));     // expected : 'hello, release'
console.log(defaultIsRelease.CONDMSG());            // expected : 'hello, release'
console.log(defaultIsRelease.CONDMSG("debug"));     // expected : 'hello, debug'
console.log(defaultIsRelease.CONDMSG("release"));   // expected : 'hello, release'

You can create conditional messages. Currently, only debug and release are available. If there is no defaultMode, it is release .


types

This is useful if you want to allow only certain types of messages.

import errorc from "error-c"

export type DefaultType =
    | bigint
    | number
    | string
    | object
    | boolean
    | ((context: Record<string, any>) => string);

const DEFINE_ERRORS = {
    NUMBER: "${value:number}",
    STRING: "${value:string}",
    OBJECT: "${value:object}",
    BOOLEAN: "${value:boolean}",    // boolean
    FUNCTION: "${value:function}",  // (context : Record<string, any>) => string
    DEFAULT: "${value}",            // infer as `DefaultType`
} as const;

const ns = errorc("namespace", DEFINE_ERRORS);

console.log(ns.NUMBER({ value: 1 }));
console.log(ns.STRING({ value: "" }));
console.log(ns.OBJECT({ value: { foo: { bar: "foo-bar" } } }));
console.log(ns.BOOLEAN({ value: true }));
console.log(ns.FUNCTION({ value: (context) => "function" }));

console.log(ns.DEFAULT({ value: 1 }));
console.log(ns.DEFAULT({ value: {} }));
console.log(ns.DEFAULT({ value: { foo: { bar: "foo-bar" } } }));
console.log(ns.DEFAULT({ value: true }));
console.log(ns.DEFAULT({ value: (context) => "function" }));

Type constaints

WARNING

The type provided by errorc is not a typescript type,

But a type defined separately by errorc.

Don't confuse this.


Redefinition output type

If you want to make the output type into class or object not string, you can use the like this.

import errorc from "error-c";

const DEFINE_ERRORS = {
    EOF: "End Of File",
    UnexpectedError: {
        debug: "unexpected error cause '${cause}'",
        release: "unexpected error",
    },
} as const;

class CustomError extends Error {
    readonly code: string
    readonly context: Record<string, any>
    constructor(message: string, code: string, context: Record<string, any>) {
        super(message)
        this.code = code
        this.context = context
    }
}

const ns = errorc(
    DEFINE_ERRORS,
    {
        handler: (message, code, context) => new CustomError(message, code, context)
    }
);

// CustomError(
//     "unexpected error cause '${secret data for debug}'",
//     "UnexpectedError",
//     {
//         cause : "secret data for debug"
//     }
// )
console.log(ns.UnexpectedError({ cause: "secret data for debug" }, "debug"))


// CustomError(
//     "unexpected error",
//     "UnexpectedError",
//     {}
// )
console.log(ns.UnexpectedError({ cause: "secret data for debug" }, "release"))

handler automatically determine whether it's debug or release, provide the appropriate context

You can use context, code, and messages to emit other type instead of string.

More examples

Many example can be found here

How does it work?

I stumbled across some interesting code here

type ExtractSemver<SemverString extends string> =
  SemverString extends `${infer Major}.${infer Minor}.${infer Patch}`
    ? { major: Major; minor: Minor; patch: Patch }
    : { error: "Cannot parse semver string" };

Surprisingly, typescript able to cut some of the parts from literals.

Using this, I cut and pasted literal, and created the package by using generics as a kind of function.

I also think this method will be useful for text template libraries like internationalization(i18n).

About

error-c is constant based error define micro framework, you can define error with message(dynamic) more staticaly and safely on typescript

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published