Skip to content

Instantly share code, notes, and snippets.

@wzulfikar
Last active December 26, 2022 23:46
Show Gist options
  • Save wzulfikar/972b3d2efd4bc38a29319e4c9e494ec1 to your computer and use it in GitHub Desktop.
Save wzulfikar/972b3d2efd4bc38a29319e4c9e494ec1 to your computer and use it in GitHub Desktop.
Typescript helper to define errors.
type ErrorFunc<ErrorMap extends Record<string, string>> = <
  Kind extends keyof ErrorMap,
  Args extends [kind: Kind, ctx?: string]
>(...args: Args) => {
  ok: false,
  error: Args[1] extends string ? {
    kind: Args[0],
    message: ErrorMap[Args[0]],
    ctx: Args[1]
  } : {
    kind: Args[0],
    message: ErrorMap[Args[0]],
  }
};

const defineErrors = <
  Key extends string,
  Value extends string,
  ErrorMap extends Record<Key, Value>
>(errorMap: ErrorMap): ErrorFunc<ErrorMap> => {
  return (kind, ctx?: string) => ({
    ok: false,
    error: {
      kind: kind,
      message: errorMap[kind],
      ctx: ctx
    }
  });
};

// --- Example

// Define all possible errors
const error = defineErrors({
  NOT_FOUND: "user not found",
  INVALID_ID: "invalid id"
})

// Trigger the error
const a = error("NOT_FOUND")
const b = error("NOT_FOUND", 'some optional context for the error')
const C = error("NOT_FOUNDD")

// More example: check if error is of specific kind
const err = Math.random() > 0.5 ? error("NOT_FOUND") : error("INVALID_ID")
if (err.error.kind == "NOT_FOUND") {
  console.log("encountered 'NOT_FOUND' error")
} else if (err.error.kind == "INVALID_ID") {
  console.log("encountered 'NOT_FOUND' error")
}

View in TS Playground

@wzulfikar
Copy link
Author

defineErrors provides a way to define all possible errors at compile time:

image

The error function produced by defineErrors correctly resolves the error at compile time:

image

An optional context message (ctx) is also supported:

image

The error function also prevents wrong error to be used:

image

@wzulfikar
Copy link
Author

wzulfikar commented Dec 26, 2022

Combine the defineErrors with Result type to create result container:

// --- Add "Result" type to create "result" container
type Result<
  TOk,
  TError extends ReturnType<ErrorFunc<any>>
> = TOk | TError;
type Ok<T> = {
  ok: true;
} & T;
function ok<TOk>(result: TOk): Ok<TOk> {
  return {
    ok: true,
    ...result
  };
}

// --- Example of using result container in a fetch function
type User = {
  id: string;
  name: string;
}
const userError = defineErrors({
  NOT_FOUND: "user not found"
})
type UserResult = Result<Ok<{user: User}>, ReturnType<typeof userError>>;

const fetchUser = (userId: string): Promise<UserResult> => 
  fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
    .then((res) => res.json())
    .then((json) => json.id ? ok({user: json}) : userError("NOT_FOUND"))

const user = fetchUser("1").then((result) => result.ok ? result : result)

View in TS Playground

With result container, you can see all possible outcomes of the function (eg. "ok" for happy path, error for other) in compile time:

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment