shield()

Environment: Server.

We use shield() to guarantee the type of telefunction arguments. (As explained in RPC, telefunctions are public and need protection.)

// CreateTodo.telefunc.js
// Environment: Server
 
export { onNewTodo }
 
import { shield } from 'telefunc'
const t = shield.type
 
shield(onNewTodo, [t.string])
async function onNewTodo(text) {
  // `text` is guaranteed to be a `string`: if `onNewTodo(42)` is called then Telefunc
  // throws an error that `text` should be a `string` (instead of a `number`)
}

If we use TypeScript, then Telefunc automaticaly defines shield(), see TypeScript - Automatic.

TypeScript - Automatic

If we use TypeScript, then Telefunc automatically generates shield() for each telefunction.

In other words: telefunction argument types are automatically validated at runtime:

// hello.telefunc.ts
 
// We don't need to define a shield() when using TypeScript: Telefunc automatically generates
// it for us. For example here, Telefunc automatically aborts the telefunction call if the
// argument is `hello({ name: 42 })` and throws an error that `name` should be a `number`.
export async function hello({ name }: { name: string }) {
   return `Welcome to Telefunc, ${name}.`
}

With Telefunc, not only can we seamlessly re-use types across our frontend and backend code, but we also get automatic type-safety at runtime. If we use a TypeScript ORM (e.g. Prisma) or SQL builder (e.g. Kysely and others), then we get end-to-end type safety all the way from database to frontend.

For a faster development, Telefunc doesn't generate shield() and your telefunction arguments aren't validated during development. Telefunc only generates shield() upon building your app for production. You can enable the generation of shield() for development by setting config.shield.dev to true.

Telefunc's automatic shield() generation only works for stacks that transpile server-side code (Next.js, Vite, Vike, SvelteKit, Nuxt, etc.).

For stacks that don't transpile server-side code (e.g. React Native, CRA, Parcel), we need to define shield() manually ourselves: see TypeScript - Manual.

TypeScript - Manual

If we define shield() manually (instead of using Telefunc's automatic shield() generator as described in TypeScript - Automatic), then note that we don't need to define the arguments type twice:

import { shield } from 'telefunc'
 
export const onNewTodo = shield(
  [shield.type.string],
  async function (text) {
    // ✅ TypeScript knows that `text` is of type `string`
  }
)

Note that the following doesn't work:

import { shield } from 'telefunc'
 
shield(onNewTodo, [shield.type.string])
// TypeScript cannot infer the type of named functions by design.
export async function onNewTodo(text) {
  // ❌ TypeScript doesn't know that `text` is of type `string`
}

Common types

Examples showcasing the most common shield() types:

// TodoList.telefunc.js
// Environment: Node.js
 
import { shield } from 'telefunc'
const t = shield.type
 
shield(onTextChange, [t.number, t.string])
async function onTextChange(id, text) {
  // typeof id === 'number'
  // typeof text === 'string'
}
 
shield(onCompletedToggle, [{ id: t.number, isCompleted: t.boolean }])
async function onCompletedToggle({ id, isCompleted }) {
  // typeof id === 'number'
  // typeof isCompleted === 'boolean'
}
 
shield(onTagListChange, [t.array(t.string)])
async function onTagListChange(tagList) {
  // tagList.every(tagName => typeof tagName === 'string')
}
 
shield(onNewMilestone, [{
  name: t.string,
  deadline: t.nullable(t.date),
  ownerId: t.optional(t.number)
}])
async function onNewMilestone({ name, deadline, ownerId }) {
  // typeof name === 'string'
  // deadline === null || deadline.constructor === Date
  // ownerId === undefined || typeof ownerId === 'number'
}
 
shield(onStatusChange, [t.or(
  t.const('DONE'),
  t.const('PROGRESS'),
  t.const('POSTPONED')
)])
async function onStatusChange(status) {
  // status === 'DONE' || status === 'PROGRESS' || status === 'POSTPONED'
}

All types

List of shield() types:

const t = shield.typeTypeScriptJavaScript
t.stringstringtypeof value === 'string'
t.numbernumbertypeof value === 'number'
t.booleanbooleanvalue === true || value === false
t.dateDatevalue.constructor === Date
t.array(T)T[]value.every(element => isT(element))
t.object(T)Record<string, T>Object.values(value).every(v => isT(v))
{ k1: T1, k2: T2, ... }{ k1: T1, k2: T2, ... }isT1(value.k1) && isT2(value.k2) && ...
t.or(T1, T2, ...)T1 | T2 | ...isT1(value) || isT2(value) || ...
t.tuple(T1, T2, ...)[T1, T2, ...]isT1(value[0]) && isT2(value[1]) && ...
t.const(val)val as constvalue === val
t.optional(T)T | undefinedisT(value) || value === undefined
t.nullable(T)T | nullisT(value) || value === null
t.anyanytrue