The working developer's guide to using an ST codebase.


Core Concept: fn()

fn() defines a function with:

  1. A function body
  2. A "prop definition" (parameters, dependencies, inputs, defaults, errors that might throw)
// can only ever be one param object,
// usually named `props`
// |
// v
const myFunc = fn(props => {
//
// implement the
// function here
//
},
// prop definition is
// any params passed to fn()
// after your function body.
)

The prop definition determines:

  • What inputs are required
  • What dependencies may be injected
  • What errors may be returned
  • What the function's final return type is

Calling The Function

Functions created with fn are called like normal functions:

myFunc({ /* props */ })

You will NEVER call the function like this:

// ❌, props objects are not strings.
myFunc("Hello")
// ❌, props objects are not numbers.
myFunc(12)
// ❌, you only pass ONE props obj.
myFunc({a: 3}, {b: 4})

You will nearly always:

// ✅, props obj is passed as the whole object.
myFunc(props)
// ✅, props obj is spread but new props added.
myFunc({...props, a: 3, b: 3})
// ⚠️, only when no props obj exists (at the top level).
myFunc({a:3, b: 4})

Declaring Function Parameters (Explicit Param Props)

Most commonly, parameters are declared explicitly using a TypeScript type cast (as):

const add = fn(props => {
props.a + props.b
// Put what data you want
// the function to have
// TypeScript's as |
// comes after an |
// empty object. |
// Then our obj type |
// | |
// | v
// v
}, {} as { a: number; b: number })

Below is a bad example which will NOT work:

// ❌, this annotation overrides ST's assigned type
const badAdd = fn((props: {a: number, b: number}) => {
props.a + props.b
})

You will NEVER see fn((props: {someParam: SomeType}) => { because it is invalid in ST. But explicit parameters passed in the prop definition will not always be an object type literal like in the first example. A codebase that commonly reuses types may do this:

type FirstName = {firstName: string}
type LastName = {lastName: string}
// ✅, this is perfectly fine, some codebases prefer aliasing their prop types.
const printName = fn(props => {
console.log(props.firstName, props.lastName)
}, {} as FirstName & LastName)

A codebase that uses runtime validators may not even use an empty JS object literal at all:

import {ValidFirstName, ValidLastName} from "./zod-prop-types"
// ✅, this is perfectly fine, some codebases use JS + TS validation.
const printName = fn(props => {
console.log(props.firstName, props.lastName)
}, ValidFirstName, ValidLastName)

If your function needs a callback or event handler passed to it, this is no different than other types:

const Button = fn(props => {
props.onClick()
}, {} as {onClick: () => void})
Button({onClick: () => console.log("Clicked!")})
// Or, in JSX
<Button onClick={() => console.log("Clicked!")} />

Some codebases will use JS object literal shorthand to make this more concise. It might look like a real JS function is passed inside the JS object, but at runtime, ST will ignore that.

const getAuthFromFile = fn(props => "")
const getUserFriends = fn(props => {
props.getAuthFromFile({}) // "user-secret-key"
}, {getAuthFromFile})
getUserFriends({
getAuthFromFile: () => "user-secret-key"
})
getUserFriends({}) // TypeScript Error: Missing property "getAuthFromFile"

Inferring Function Parameters (Implicit Param Props)

In addition to explicitly declaring parameters, ST can infer required parameters or errors from other ST functions.

This happens when you pass a bare function directly into the prop definition list.

fn(props => {
// ...
}, loginUserOrFail)

When a bare function is passed in the prop defs the function body will now:

  • Require all parameters that loginUserOrFail requires
  • Declare all errors that loginUserOrFail may return
  • Keep the current function in sync if loginUserOrFail changes later

This is commonly used when you want to call a function without caring how it is called.

const loginUserOrFail = fn(props => {
// requires userName, password
// may return LoginErr
}, {} as { userName: string; password: string }, LoginErr)
const getDashboard = fn(props => {
// ✅, You can call it directly.
// No need to know what it takes
loginUserOrFail(props)
}, loginUserOrFail)

The above example results in:

  • getDashboard now requires userName and password
  • getDashboard now returns Dashboard | MayFail<R, LoginErr>
  • No explicit prop typing was written If loginUserOrFail later adds a parameter or error, getDashboard will update automatically.

Why the function is not on props?
When passed bare, the function is not available as props.loginUserOrFail.

fn(props => {
loginUserOrFail(props) // ✅
props.loginUserOrFail() // ❌ does not exist
}, loginUserOrFail)

This is intentional:

  • Bare functions exist only to declare requirements
  • They do not create named dependencies

Making it available on props
If you want both behaviors (parameter inference, a named dependency) you can include both forms:

// Both forms allow props.loginUserOrFail
// This is the explicit required prop form.
fn(props => {
props.loginUserOrFail(props)
}, loginUserOrFail, { loginUserOrFail })
// This is the explicit default provided prop form.
// Default() is covered later in this article.
fn(props => {
props.loginUserOrFail(props)
}, loginUserOrFail, Default({ loginUserOrFail }))

Implicit param props vs explicit props

TechniqueWhen to use
Explicit object propsYou want to control or document inputs
Bare function propsYou want inputs + errors to follow another function
BothYou want inference also a named dependency

Common usage pattern
Implicit param props are frequently used to push parameters, errors outward:

const coreLogic = fn(props => {
// no explicit params here
}, loginUserOrFail, fetchUserData, saveAuditLog)

The core function stays simple, while callers absorb:

  • All required parameters
  • All possible error types This pattern is common near the "outer edge" of an application (routes, handlers, jobs).

Summary

  1. Passing a bare function into prop defs implicitly declares prop defs
  2. The function is not placed on props
  3. Signatures stay up to date automatically
  4. This is one of the most common ways parameters, errors propagate

Default Function Parameters (Explicit Default Props)

By default, all props declared in a function's prop definition are required at the call site.

const printName = fn(props => {
console.log(props.firstName, props.lastName)
}, {} as { firstName: string, lastName: string })
printName({}) // TypeScript Error: "firstName", "lastName" not provided

Providing defaults for individual props
You can supply defaults for one or more props using Default(...). Only the remaining props stay required.

const printName = fn(props => {
console.log(props.firstName, props.lastName)
}, {} as { lastName: string }, Default({ firstName: "Glacier" }))
printName({}) // TypeScript Error: "lastName" not provided

In this example:

  • firstName is optional at the call site
  • lastName is still required

Providing defaults for all props
If all props are defaulted, the function can be called with an empty object.

const printName = fn(props => {
console.log(props.firstName, props.lastName)
}, Default({ firstName: "Glacier", lastName: "Ocean" }))
printName({}) // ✅, Glacier Ocean

Defaults can always be overridden
Defaulted values are only fallbacks. Callers may override them by passing explicit values.

printName({ firstName: "Arctic" })
// logs: Arctic Ocean
printName({ firstName: "Arctic", lastName: "Sea" })
// logs: Arctic Sea

Defaults do not lock values or restrict overrides.


Defaulting functions
Functions can be defaulted the same way as values.

const print = fn(props => {
}, {} as {name: string})
const logName = fn(props => {
props.print(props)
}, {} as { name: string }, Default({ print }))
// uses default print
logName({ name: "Glacier" })
// prints: "Glacier"
// uses own no-op print
logName({ name: "Glacier", print: () => {} })
// does not print.

Summary

  • Props are required unless defaulted
  • Default(...) makes props optional at the call site
  • Defaults apply only when a value is not provided
  • All defaults can be overridden
  • Functions and values follow the same defaulting rules

Throwing Errors, Declaring Errors, A | MayFail<R, E>

ST treats errors as data that flows through functions, rather than manually managed control flow.

At runtime: try/catch is never needed for errors.
At compile time: errors are declared, tracked, then eliminated through types.

You will mostly interact with errors by:

  • Declaring them
  • Passing props
  • Letting TypeScript decide whether an error is visible or eliminated

Declaring errors
Including an error type in the prop definition means the function may return that error:

const getToken = fn(props => {
// We `throw` the error we declared.
if (props.token != "secret-key")
throw new AuthError({})
return "logged-in-token-123"
}, {} as {token: string}, AuthError)

Even though the function uses throw, you do NOT need try/catch. Instead, the return type of this function will be string | AuthError.

const printToken = fn(props => {
// What does TypeScript think `tokenResult` is?
// NOT just `string`
// It shows `string | AuthError`
// |
// v
const tokenResult = getToken({ token: props.token })
// Reminder:
// We can use "Implicit Param Props"
// as described so we do not need to
// redeclare our params or errors
// |
// v
}, getToken)

getToken will never need a try/catch. It will simply return the error. Because we declared the error, it will show up in the return type to TypeScript.


Eliminating Errors
It is absolutely possible to call getToken but not see any errors returned. This is the most common pattern in codebases. Any time you call getToken while having the same errors declared, you can easily opt-in to hiding the errors because you would never see them at runtime anyway. Look one more time at our printToken example.

const printToken = fn(props => {
// ⚠️, It shows `string | AuthError`
// |
// v
const tokenResult1 = getToken({ token: props.token })
// ✅, It shows `string`
// |
// v
const tokenResult2 = getToken(props)
// ✅, It shows `string`
// |
// v
const tokenResult3 = getToken({...props})
// ✅, It shows `string`
// |
// v
const tokenResult3 = getToken({...props, token: "my-bad-token"})
// Reminder:
// "Implicit Param Props" means our
// function is declaring all the params / errors
// of `getToken`, so we can opt-in to hiding its
// errors. This is exactly as intended.
// |
// v
}, getToken)

What went wrong in the first call to getToken?

  • 🟢 all calls were in a function correctly declaring the error
  • 🟢 all call also correctly passed a token
  • 🔴 only the calls that passed the whole props object eliminated the errors

You must pass the whole props object to remove any errors.

The errors you declare are symbolically stored in the props. Also, only the errors you declare are removed, that is why Implicit Param Props are more commonly used, because TS already knows just the errors you need to declare.


How to read a MayFail
The typical type of your function if you never declared errors would be:

const nameInitial = fn(props => {
return props.name[0]
}, {} as {name: string})
// `nameInitial`
// `(props: {name: string}) => string`

This is no different than had you removed the ST library or wrote this function vanilla. But when declaring errors, the new type of your function can appear tricky at first glance.

const nameInitial = fn(props => {
const initialLetter = props.name[0]
if (initialLetter == "A")
throw new BannedInitialA({})
return initialLetter
}, {} as {name: string}, BannedInitialA)
// `nameInitial`
// `<Err>(props: {name: string} & Err) => string | MayFail<Err, E>`

This type allows you to call nameInitial without declaring any errors if you want to. If you do not declare your errors you'll get this default:

// [original ] nameInitial: <Err>(props: {name: string} & Err) => string | MayFail<Err, BannedInitialA>
// [calling ] nameInitial: <{}>(props: {name: string}) => string | MayFail<{}, BannedInitialA>
// [returning] nameInitial: (props: {name: string}) => string | BannedInitialA
const result = nameInitial({name: "Petra"})
// `result`
// `string | BannedInitialA`

This type also allows you to call nameInitial using any of the methods for declaring errors you prefer, to result in this:

// [original ] nameInitial: <Err>(props: {name: string} & Err) => string | MayFail<Err, BannedInitialA>
// [calling ] nameInitial: <BannedInitialA>(props: {name: string}) => string | MayFail<BannedInitialA, BannedInitialA>
// [resolving] nameInitial: <BannedInitialA>(props: {name: string}) => string | (BannedInitialA - BannedInitialA)
// [returning] nameInitial: (props: {name: string}) => string
const result = nameInitial({...props, name: "Petra"})
// `result`
// `string`

Two Errors Helpers
You will see two functions exports NamedThrow or NamedError, they both can easily be used to create errors for your application.
When you see errors which use NamedError under the hood, these errors:

  • Real JS Error instances
  • Full stacktraces
  • Often reused through the application
  • Stacktrace helps locate in logs
  • Error objects cannot be sent over HTTP by default

When you see NamedThrow used to create error types:

  • Plain JS objects (POJO)
  • Cheap, no stacktrace
  • Best thrown from a single function (e.g., a validator)
  • JSON-ready
  • Can be thrown all the way to the browser if desired

Both objects have a property .as which has its error name.
Instead of using instanceof you can freely use matchAs(resultObjOrError) from @arksouthern/stx to exhaustive match on the result at the boundary of your code whether frontend or backend.


First-Param-String & Readable JS Error Stackframes

You may see fn() called including a string literal as the first param.

const getFriendsApi = fn("get-friends user.id + limit", props => {
// ...
}, {} as { userId: number; limit: number })

This text is an optional param you can pass to fn() when defining your function. If you pass this string, any exceptions throws that are not part of your ST errors (like fetch() to a bad domain, or readFile on a locked file), these JS exceptions will show this string in the stack trace. The benefit to exception logging or API usage logging is why you are likely to see this style in some codebases.


(1) Defining a function.

          ┌─────────┐ 
          │         │ 
          │   fn()  │ 
          │         │ 
          └────┬────┘ 
               │       ┌──────────┐  ┌─────────────┐
               │       │          │  │             │
               │       │ funcBody │  │ ...propDefs │               
               │       │          │  │             │               
               │       └─────┬────┘  └─────┬───────┘
               │             │             │    
               │             │             │       ┌────────────────────────┐
               │             │             │       |                        |
               └────────────► ────────────► ──────►| R => A | MayFail<R, E> |
                                                   |                        |
                                                   └────────────────────────┘


(2a) Calling a function. All errors (E) are passed in props when calling.

    ┌────────────────────────┐
    |                        |
    | R => A | MayFail<R, E> |
    |                        |
    └────────────────────────┘
               │       ┌──────────┐  ┌─────────────┐
               │       │          │  │             │
               │       │   props  │  │  allErrors  │               
               │       │          │  │             │               
               │       └─────┬────┘  └─────┬───────┘
               │             │             │    
               │             │             │       ┌────────────────────┐
               │             │             │       |                    |
               └────────────► ────────────► ──────►| A (Success Return) |
                                                   |                    |                   
                                                   └────────────────────┘



(2b) Calling a function. No errors or only some errors are passed in props when calling.

    ┌────────────────────────┐
    |                        |
    | R => A | MayFail<R, E> |
    |                        |
    └────────────────────────┘
               │       ┌──────────┐ 
               │       │          │ 
               │       │   props  │ 
               │       │          │ 
               │       └─────┬────┘ 
               │             │                  
               │             │           ┌────────────────────┐
               │             │           |                    |
               └────────────► ──────────►|  A or E (not in R) |
                                         |                    |                   
                                         └────────────────────┘

One Page README Guide To ST
ArkSouthern

AuthoredSep 22nd 2022
UpdatedJan 24th 2026
IDn-rd-stg