Skip to content

contracts ​

Extremely small library (less than 831 B controlled by CI) for creating Contracts that allows you to introduce data validation on edges of the application with no performance compromises.

Installation ​

First, you need to install package:

sh
pnpm install @withease/contracts
sh
yarn add @withease/contracts
sh
npm install @withease/contracts

Creating a Contract ​

@withease/contracts exports bunch of utilities that can be used to create a Contract, read the full API reference here. Any of the utilities returns a Contract object, that accepts something unknown and checks if it is something concrete defined by the used utility.

<script setup>
import { arr, num } from '@withease/contracts';

const contract = arr(num);
</script>

<template>
  <h1>
    Out <em>Contract</em> is ensuring that passed data is an array of numbers
  </h1>
  <section>
    <h2>Valid data example</h2>
    <p>Let us pass [1, 2, 3] to the <em>Contract</em></p>
    <p>isData() 👉 {{ contract.isData([1, 2, 3]) }}</p>
    <p>
      getErrorMessages() 👉
      {{ JSON.stringify(contract.getErrorMessages([1, 2, 3])) }}
    </p>
  </section>

  <section>
    <h2>Invalid data example</h2>
    <p>
      Let us pass [1, 'WHOA', 3] to the <em>Contract</em>. instead of number.
    </p>
    <p>isData() 👉 {{ contract.isData([1, 'WHOA', 3]) }}</p>
    <p>
      getErrorMessages() 👉
      {{ JSON.stringify(contract.getErrorMessages([1, 'WHOA', 3])) }}
    </p>
  </section>
</template>

Extracting types from a Contract ​

@withease/contracts provides a special type UnContract that can be used to extract a type from a Contract.

ts
import { type UnContract, obj, str, num } from '@withease/contracts';

const UserContract = obj({
  id: num,
  name: str,
  email: str,
});

// type User = { id: number, name: string, email: string }
type User = UnContract<typeof UserContract>;

Usage of a Contract ​

@withease/contracts is designed to be compatible with Effector's ecosystem without additional interop, so most of the time you can pass created Contract to other Effector's libraries as is.

Farfetched ​

Farfetched is the advanced data fetching tool for web applications based of Effector. It suggests to ensure that data received from the server is conforms desired Contract.

ts
import { createJsonQuery } from '@farfetched/core';
import { obj, str, arr, val, or } from '@withease/contracts';

const characterQuery = createJsonQuery({
  params: declareParams<{ id: number }>(),
  request: {
    method: 'GET',
    url: ({ id }) => `https://rickandmortyapi.com/api/character/${id}`,
  },
  response: {
    // after receiving data from the server
    // check if it is conforms the Contract to ensure
    // API does not return something unexpected
    contract: obj({
      id: str,
      name: str,
      status: Status,
      species: str,
      type: str,
      gender: Gender,
      origin: obj({ name: str, url: str }),
      location: obj({ name: str, url: str }),
      image: or(val('Female'), val('Male'), val('Genderless')),
      episode: arr(str),
    }),
  },
});

effector-storage ​

effector-storage is a small module for Effector to sync stores with different storages (local storage, session storage, async storage, IndexedDB, cookies, server side storage, etc).

Since data is stored in an external storage it is important to validate it before using it in the application.

ts
import { createStore } from 'effector';
import { persist } from 'effector-storage';
import { num } from '@withease/contracts';

const $counter = createStore(0);

persist({
  store: $counter,
  key: 'counter',
  // after reading value from a storage check if a value is number
  // to avoid pushing invalid data to the Store
  contract: num,
});

Integration with other libraries ​

Since @withease/contracts is compatible Contract protocol it can be used with any library that supports it.

For instance, you can define a part of a Contract with Zod and combine it with @withease/contracts:

ts
import { z } from 'zod';
import { arr, obj } from '@withease/contracts';
import { zodContract } from '@farfetched/zod';

const User = z.object({
  name: z.string(),
});

const MyContract = arr(
  obj({
    // 👇 easily integrate Zod via compatibility layer
    users: zodContract(User),
  })
);

The full list of libraries that support Contract protocol can be found here.

Differences from other libraries ​

It is extremely small and we mean it 👇

TIP

Data fetched directly from https://esm.run/ and updates on every commit.

Of course smaller size is comes with some trade-offs, but we believe that in most cases it is worth it. @withease/contracts covers most of the common cases but does not try to be a silver bullet for all possible cases. It does not aim to have the following features from other libraries:

Q: What if I started a project with @withease/contracts and then realized that I need some of the features that are not covered by it?

A: No worries! You can easily integrate @withease/contracts with other libraries that have the features you need. Check out the Integration with other libraries section for more details.

Released under the MIT License.