0.0.3 โ€ข Published 1 year ago

@jarvistack/nope v0.0.3

Weekly downloads
-
License
-
Repository
-
Last release
1 year ago

@jarvistack/nope

a functional and schema based validation library for typescript with static type inference (also for all possible errors).

Introduction

Thanks to Typescript you can catch a lot of bugs at the time of writing your code, but you will still encounter bugs at runtime if you dont carefully validate your input data. On the server you would validate inputs like:

  • request payloads
  • upstream service responses
  • environment variables

On the client your inputs come from:

  • user input (forms)
  • api responses

By carefully validating all inputs at the edges of our application we can ensure that the data at runtime really matches our types. This library comes with the following features to help you with that concern:

  • ๐Ÿ˜Ž Dont write a single type yourself, infer them from your schema!
  • ๐Ÿ’ช Strongly typed data and error objects
  • ๐Ÿคทโ€โ™‚๏ธ Automatic human friendly error messages
  • 0๏ธโƒฃ No dependencies
  • ๐Ÿชถ Lightweight ( 2.8 kB )
  • ๐ŸŒด Tree-shakeable
  • โšก Fast ( 6x faster than zod )
  • ๐Ÿงท First class support for opaque (branded) types
  • ๐Ÿ—๏ธ Create your own schemas and types, with just a few lines of code

Getting started

  1. Install: npm i -S @jarvistack/nope
  2. Define a schema and infer the type from it:
const PersonSchema = ObjectSchema({
    name: StringSchema,
    email: EmailSchema,
});

type Person = InferType<typeof PersonSchema>; // { name: string, email: Email }
  1. Validate your input data:
const input = {
    name: 'Tony Stark',
    email: 'tony@starkindustries.com',
};

const either = PersonSchema.validate(input);

if (isOk(either)) {
    // typeof person is of type `Person` in here
    const person = either.value;
} else {
    // typeof errCode: "E_OBJECT" | "E_OBJECT_MISSING_KEYS" | "E_OBJECT_ADDITIONAL_KEYS" | "E_OBJECT_PROPERTY"
    const errCode = either.value.code;
}

Thats all. Inside of the if branch it is guaranteed that your data matches the schema that you have specified and in the else branch you can see that all the possible errors are strongly typed:

  • E_OBJECT The input is not of type object
  • E_OBJECT_MISSING_KEYS The input is a object, but some keys are missing
  • E_OBJECT_ADDITIONAL_KEYS The input is a object, but there were additional keys on the input object
  • E_OBJECT_PROPERTY The input is a object and the exact amount of keys are present, but some nested validation failed.

There are more detailled informations and a automatic human readable error message available for each possible error object. Here is a interactive example.

Opague (branded) types

This library makes heavy use of opaque ( sometimes also called branded ) types. One example would be the Email type that comes from the EmailSchema. It is not a alias for the string type, but its very own type. Let me show you why this might be useful. Here you can see that we can pass a empty string to the sendEmail function and typescript will not complain ๐Ÿ˜Ÿ.

const sendEmail = (email: string) => { /* TODO: send email */ };
sendEmail('');

By declaring something with an EmailSchema, the data will be typed with an opaque (branded) type called Email. You still have all the string functions available on it and it behaves like a normal string in your code. But by declaring a argument with the Email type, you will get a type error when you try to pass a normal (potential empty) string to it:

const sendEmail = (email: Email) => { /* TODO: send email */ };

// Argument of type 'string' is not assignable to parameter of type 'Email'.
sendEmail('');

const either = EmailSchema.validate('tony@starkindustries.com');
if (isOk(either)) {
    // typeof email: `Email`
    const email = either.value;
    // it is ensured at runtime that `email` is a valid email address
    sendEmail(email);
}

We can create the Email type from just 1 place in our codebase. This adds another layer of typesafety to your application. You can avoid always validating your variables before using it, but this will also remove the runtime checks. So only use this if you know what you are doing:

// typeof email: `Email`
const email = EmailSchema.create('tony@starkindustries.com');
sendEmail(email);

If you dont like this concept of an opaque type you can always opt out of it but keep the full runtime typesafety and strongly typed errors by just using the EmailValidation with a StringSchema:

const MyStringEmailSchema = withValidations(StringSchema, [EmailValidation]);

const either = MyStringEmailSchema.validate('tony@starkindustries.com');
if (isOk(either)) {
    either.value; // typeof `either.value`: string
} else {
    const errCode = either.value.code; // typeof `errCode`: "E_STRING" | "E_EMAIL"
}

Builtin Schemas

  • ArraySchema
  • BooleanSchema
  • DateSchema
  • EmailSchema
  • IntSchema
  • Iso8601DateTimeSchema
  • Iso8601DateSchema
  • Iso8601TimeSchema
  • LiteralSchema
  • NullSchema
  • NumberSchema
  • ObjectSchema
  • RecordSchema
  • StringSchema
  • TupleSchema
  • UIntSchema
  • UndefinedSchema
  • UnionSchema
  • UuidSchema

Builtin Validations

  • ArrayValidation
  • BooleanValidation
  • DateValidation
  • EmailValidation
  • IntValidation
  • Iso8601DateTimeValidation
  • Iso8601DateValidation
  • Iso8601TimeValidation
  • LiteralValidation
  • NullValidation
  • NumberValidation
  • ObjectValidation
  • RecordValidation
  • StringValidation
  • TupleValidation
  • UIntValidation
  • UndefinedValidation
  • UnionValidation
  • UuidValidation

Real world example

const countries = ['AT', 'DE', 'CH'] as const;
const CountrySchema = UnionSchema(countries.map(LiteralSchema));

const AddressSchema = ObjectSchema({
    street: withValidations(StringSchema, [StringMinLength(1), StringMaxLength(255)]),
    zip: withValidations(StringSchema, [StringMinLength(1), StringMaxLength(255)]),
    city: withValidations(StringSchema, [StringMinLength(1), StringMaxLength(255)]),
    country: CountrySchema,
});

const themes = ['light', 'dark'] as const;
const ThemeSchema = UnionSchema(themes.map(LiteralSchema));

const PersonSchema = ObjectSchema({
    id: UuidSchema,
    name: withValidations(StringSchema, [StringMinLength(1), StringMaxLength(255)]),
    email: EmailSchema,
    birthday: Iso8601DateSchema,
    importedAt: Iso8601DateTimeSchema,
    address: ObjectSchema({
        main: AddressSchema,
        others: ArraySchema(AddressSchema),
    }),
    profile: ObjectSchema({
        theme: ThemeSchema,
    }),
});
0.0.3

1 year ago

0.0.2

1 year ago

0.0.1

1 year ago