Skip to main content
Deno 2 is finally here 🎉️
Learn more

Typescript Matches

codecov Bundle Phobia Bundle Phobia

Living Documentation https://runkit.com/blu-j/ts-matches

Uses

  • Schema Validation (parsers: like matches.string)
  • Schema Switching
  • Pattern matching
  • Switches as expressions

Related Libs

Tech Used

Wiki Pattern Matching

Also useful for casting and boundary verifications. So using this as a json validator. The benefit comes that the parser becomes a validator, also the types are given back to typescript, where something like ajv cannot do or alot of validators.

Examples

The easiest and most useful feature is using the matcher as a validation. Here I want to validate that the shape is correct or throw an error

import matches from "https://deno.land/x/ts_matches/mod.ts";
const goldFishMatcher = matches.shape({
  type: t.literal("gold-fish"),
  position: t.tuple(t.number, t.number),
  age: t.natural,
  name: t.string,
});
// For this example I'm making the shape less known
const input: object = {
  type: "gold-fish",
  position: [2, 3],
  age: 5,
  name: "Nemo",
};
// The matcher will know that the type returned is always the correct shape, and the type will reflect that
const checkedInput = goldFishMatcher.unsafeCast(input);

A variation is to use the guard version.

import matches from "ts-matches";
const goldFishMatcher = matches.shape({
  type: t.literal("gold-fish"),
  position: t.tuple(t.number, t.number),
  age: t.natural,
  name: t.string,
});
// For this example I'm making the shape less known
const input: object = {
  type: "gold-fish",
  position: [2, 3],
  age: 5,
  name: "Nemo",
};
if (!goldFishMatcher.test(input)) {
  return;
}
/// After this point typescript will know the shape will be intersecting the shape we defined in the matcher

This is useful on a boundary layer, like fetching a value. In that case we have no idea what the shape is, so we should do a check on that.

import matches from "https://deno.land/x/ts_matches/mod.ts";
fetch("fishes.com/gold-fishes/12")
  .then((x) => x.json())
  .then(
    matches.shape({
      type: t.literal("gold-fish"),
      position: t.tuple(t.number, t.number),
      age: t.natural,
      name: t.string,
    }).unsafeCast,
  );

And when we get the value out it will either be the type that we want, or it will throw an error. The other use case is a pattern matching.

import matches from "matches";
const getText = (x: unknown): string =>
  matches(x)
    .when(matches.string, (value) => `Found string: ${value}`)
    .when(matches.number, (value) => `Found number + 1: ${value + 1}`)
    .defaultTo("no found type yet");

And here we can use the type checking and what do in that case. With destructuring, lots of abilities are there

import matches from "matches";
const matchNone = matches.tuple(matches.literal("none"));
const matchSome = matches.tuple(matches.literal("some"), matches.any);
type option = ReturnType<typeof matchNone.unsafeCast> | typeof matchSome._TYPE;
const matchInteger = matches.every(
  matchSome,
  matches.tuple(matches.any, matches.number),
);
const testValue = ["some", 3];
const currentValue = matches(testValue)
  .when(matchNone, () => 0)
  .when(matchInteger, ([, value]) => value + 1)
  .when(matchSome, () => 0)
  .defaultTo(0);

We can also use the matches to match on literals, or return literals

import matches from "matches";
const currentValue = matches("5" as const)
  .when("5", "6", "At 5 or 6")
  .unwrap(0);

API

Given that the default export is matches Then the type of matches is unkown -> matcherChain, and also has the properties on that function that return a parser or a function that creates a parser

Attribute Description
array Testing that any array is good
arrayOf Testing that any array is good and filled with type passed in
some That one of the matchers pass
tuple That we match a tuple of parsers
regex That we match the passed in regex
number Number
natural Number > 0 and is integer
isFunction is a function
object is an object
string is a string
shape Matches a shape of an object, shape({key: parser}, ["key"], {"key": "fallbackValue"} as const) for optionals default fallbacks,shape({key: parser}, ["key"]) for optionals no defaults, shape({key: parser}) for every required
partial Matches a shape of maybe attributes
literal Matches an exact match
every Matches every match passed in
guard Custom function for testing
any is something
boolean is a boolean
nill is a null or undefined
dictionary sets of [parserForKey, parserForValue] to validate a dictionary/ mapped type
recursive A way of doing a recursive parser, passing the self. Note this requires the type before while creating, cannot go from creation side.
deferred A way of creating a type that we will be filling in later, will be using the typescript shape first to verify
literals One the literals passed through

MatcherChain api

Attribute Description
when Create a matching case, when match return value
defaultTo Fall through case, ensures all are caught
defaultToLazy Fall through case, ensures all are caught in lazy fashion

Parser api

Attribute Description
parse Use this to turn a value into an either
usafeCast Use this to get the value or throw an error
castPromise Cast into a promise
optional output type is now a null of value
defaultTo instead of creating a optional we fallback to a value
refine we want to add more tests to value, could change type to sub
validate we want to add more tests to value
errorMessage If validation would create an error, return error as string, else void
test A guard for the type, returns true if type is valid (as a x is type)

Parser.parserErrorAsString ( validationError: parserError ): string This is the exposed transform of the parserError to a string. Override this if you want to make the errors different.

And of of any matcher we two functions, refine and unsafe cast. Refine is useful when we want to check a condition, like is even. And the matcher is also a function which creates an either of our value as well.

Deploying

Use the npm version minor | major and push the tags up, Then publish via npm