TinyBase logoTinyBase β

Schema-Based Typing

You can use type definitions that infer API types from the schemas you apply, providing a powerful way to improve your developer experience when you know the shape of the data being stored.

The schema-based definitions can be accessed by adding the with-schemas suffix to your imports. For example:

import {createStore} from 'tinybase/with-schemas'; // NB the 'with-schemas'

const store = createStore().setValuesSchema({
  employees: {type: 'number'},
  open: {type: 'boolean', default: false},
});

store.setValues({employees: 3}); //                      OK
store.setValues({employees: true}); //                   TypeScript error
store.setValues({employees: 3, website: 'pets.com'}); // TypeScript error

In this example, the store is known to have the ValuesSchema provided, and all relevant methods will have type constraints accordingly, even for listeners:

store.addValueListener(null, (store, valueId, newValue, oldValue) => {
  valueId == 'employees'; // OK
  valueId == 'open'; //      OK
  valueId == 'website'; //   TypeScript error

  if (valueId == 'employees') {
    newValue as number; //   OK
    oldValue as number; //   OK
    newValue as boolean; //  TypeScript error
    oldValue as boolean; //  TypeScript error
  }
  if (valueId == 'open') {
    newValue as boolean; //  OK
    oldValue as boolean; //  OK
  }
});

Getting the Typed Store

Only the setSchema method, setTablesSchema method, and setValuesSchema method return a typed Store object. So, to benefit from the typing, ensure you assign your Store variable to what those methods return, rather than just the createStore function.

For example, the following will work at runtime, but you will not benefit from the developer experience of typing on the store variable as we did in the example above.

import {createStore} from 'tinybase/with-schemas';

const store = createStore(); // This is not a schema-typed Store

store.setValuesSchema({
  employees: {type: 'number'},
  open: {type: 'boolean', default: false},
}); // Instead you should use the return type from this method

One further thing to be aware of is that for the typing to work effectively, the schema must be passed in directly, or, if it is a variable, as a constant:

const valuesSchema = {
  employees: {type: 'number'},
  open: {type: 'boolean', default: false},
} as const; // NB the `as const` modifier
store.setValuesSchema(valuesSchema);

It's worth noting that typing will adapt according to schemas being added, removed, or changed:

const tablesSchema = {
  pets: {species: {type: 'string'}},
} as const;

const valuesSchema = {
  employees: {type: 'number'},
  open: {type: 'boolean', default: false},
} as const;

const store = createStore();
const storeWithBothSchemas = store.setSchema(tablesSchema, valuesSchema);
const storeWithJustValuesSchema = storeWithBothSchemas.delTablesSchema();
const storeWithValuesAndNewTablesSchema = storeWithBothSchemas.setTablesSchema({
  pets: {
    species: {type: 'string'},
    sold: {type: 'boolean', default: false},
  },
});

Typing The ui-react Module

Schema-based typing for the ui-react module is handled a little differently, due to the fact that all of the hooks and components are top level functions in the module. It would be frustrating to apply a schema to type each and every one in turn.

Instead, you can use the WithSchemas type (which takes the typeof the schemas), and the following pattern after your import. This applies the schema types to the whole module en masse, and then you can select the hooks and components you want to use:

import * as UiReact from 'tinybase/ui-react/with-schemas';
import React from 'react';
import {createStore} from 'tinybase/with-schemas';

const tablesSchema = {
  pets: {species: {type: 'string'}},
} as const;
const valuesSchema = {
  employees: {type: 'number'},
  open: {type: 'boolean', default: false},
} as const;

// Cast the whole module to be schema-based with WithSchemas:
const UiReactWithSchemas = UiReact as UiReact.WithSchemas<
  [typeof tablesSchema, typeof valuesSchema]
>;
// Deconstruct to access the hooks and components you need:
const {TableView, useTable, ValueView} = UiReactWithSchemas;

const store = createStore().setSchema(tablesSchema, valuesSchema);
const App = () => (
  <div>
    <TableView store={store} tableId="species" /> {/*   OK               */}
    <TableView store={store} tableId="customers" /> {/* TypeScript error */}
    {/* ... */}
  </div>
);

Note that in React Native, the resolution of modules and types isn't yet quite compatible with Node and TypeScript. You may need to try something like the following to explicitly load code and types from different folders:

import * as UiReact from 'tinybase/ui-react'; // code
import React from 'react';
import type {WithSchemas} from 'tinybase/ui-react/with-schemas'; // types
import {createStore, TablesSchema, ValuesSchema} from 'tinybase/with-schemas';

const tablesSchema = {
  pets: {species: {type: 'string'}},
} as const;
const valuesSchema = {
  employees: {type: 'number'},
  open: {type: 'boolean', default: false},
} as const;

const UiReactWithSchemas = UiReact as unknown as WithSchemas<
  [typeof tablesSchema, typeof valuesSchema]
>;

//...

Multiple Stores

In the case that you have multiple Store objects with different schemas, you will need to use WithSchemas several times, and deconstruct each, something like this:

const UiReactWithPetShopSchemas = UiReact as UiReact.WithSchemas<
  [typeof petShopTablesSchema, typeof petShopValuesSchema]
>;
const {
  TableView: PetShopTableView,
  useTable: usePetShopTable,
  ValueView: usePetShopValueView,
} = UiReactWithPetShopSchemas;

const UiReactWithSettingsSchemas = UiReact as UiReact.WithSchemas<
  [typeof settingsTablesSchema, typeof settingsValuesSchema]
>;
const {
  TableView: SettingsTableView,
  useTable: useSettingsTable,
  ValueView: useSettingsValueView,
} = UiReactWithSettingsSchemas;

const petShopStore = createStore().setSchema(
  petShopTablesSchema,
  petShopValuesSchema,
);
const settingsStore = createStore().setSchema(
  settingsTablesSchema,
  settingsValuesSchema,
);
const App = () => (
  <div>
    <PetShopTableView store={petShopStore} tableId="species" />
    <SettingsTableView store={settingsStore} tableId="viewSettings" />
    {/* ... */}
  </div>
);

Summary

Schema-based typing provides a powerful developer-time experience for checking your code and autocompletion in your IDE. Remember to use the with-schema suffix on the import path and use the patterns described above.

We move on to discussing more complex programmatic enforcement of your data, and for that we turn to the Mutating Data With Listeners guide.