Skip to content

davidkarolyi/tguard

Repository files navigation

tguard 💂

Declarative type guarding system for TypeScript.

CI codecov

Installation

npm install tguard

or

yarn add tguard

Example Usage

import {
  TArray,
  TInteger,
  TObject,
  TString,
  TStringUUID,
  GuardedType,
} from "tguard";

// Let's define a User type as a Guard.
const TPost = TObject({
  id: TStringUUID,
  title: TString,
  body: TString,
});

const TUser = TObject({
  id: TStringUUID,
  name: TString,
  age: TInteger,
  posts: TArray(TPost),
});

// Note: If you don't want to define these types twice
// (once as a TypeScript type, once as a guard)
// you can infer it's guarded types with the `GuardedType` utility type:
type User = GuardedType<typeof TUser>;
type Post = GuardedType<typeof TPost>;

// We can use guards to validate if a given value is a valid 'User' type or not:
if (TUser.isValid(unknownValue)) {
  // TypeScript will know that `unknownValue` is 'User' in this block.
}

// Or try to cast a value to the User type:
try {
  const user = TUser.cast({ posts: ["Who am I?", "I am a user."] });
  // Type of `user` === {
  //    id: string,
  //    name: string,
  //    age: number,
  //    posts: Array<{id: string, title: string, body: string}>
  // }
} catch (error) {
  // error.message === 'Validation failed: Missing value at "id", expected type: string(UUID)'
}

Motivation, Guarding Types Manually ❌

TypeScript does a static analysis to infer types, but won't provide any guarantees for runtime type safety. These checks should be done by the developer manually.

Here is an example for that with using type predicates:

❌ Without tguard:

interface User {
  name: string;
  posts: string[];
}

function isUser(fetchedUser: any): fetchedUser is User {
  const user = fetchedUser as User;
  return typeof user.name === "string" && isStringArray(user.posts);
}

function isStringArray(array: any): array is string[] {
  if (!Array.isArray(array)) return false;
  for (const item of array) {
    if (typeof item !== "string") return false;
  }
  return true;
}

✅ With tguard

import { TObject, TString, TArray } from "tguard";

const TUser = TObject({
  name: TString,
  posts: TArray(TString),
});

Guards

By convention, every guard's name starts with an upper-case T. These are instances of the Guard abstract class with a name field, isValid method, and a cast method.

Built-in type guards:

Primitive Guards

Functions, returning a Guard

Defining Custom guards

You can define any custom Guard with TValidate.

Defining a guard that validates if a number is bigger than 10:

const TNumberBiggerThan10 = TValidate<number>(
  "number(bigger than 10)",
  (value) => typeof value === "number" && value > 10
);

Exported utility tpyes

Tree shaking

All guards can be imported as a single module, which enables tree-shaking:

import TString from "tguard/lib/guards/TString";
import Guard, { GuardedType } from "tguard/lib/Guard";