The working developer's guide to using an ST codebase.
Core Concept: fn()
fn() defines a function with:
- A function body
- A "prop definition" (parameters, dependencies, inputs, defaults, errors that might throw)
// can only ever be one param object,// usually named `props`// |// vconst 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 typeconst 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"The order of your prop types does not matter at all. You will see our examples place explicit object literal props first, but some put the errors first. ST will handle whatever preference.
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
loginUserOrFailrequires - Declare all errors that
loginUserOrFailmay return - Keep the current function in sync if
loginUserOrFailchanges 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:
getDashboardnow requiresuserNameandpasswordgetDashboardnow returnsDashboard | MayFail<R, LoginErr>- No explicit prop typing was written
If
loginUserOrFaillater adds a parameter or error,getDashboardwill 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
| Technique | When to use |
|---|---|
| Explicit object props | You want to control or document inputs |
| Bare function props | You want inputs + errors to follow another function |
| Both | You 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
- Passing a bare function into prop defs implicitly declares prop defs
- The function is not placed on props
- Signatures stay up to date automatically
- 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 providedProviding 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 providedIn this example:
firstNameis optional at the call sitelastNameis 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 OceanDefaults 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 SeaDefaults 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 printlogName({ name: "Glacier" })// prints: "Glacier"
// uses own no-op printlogName({ 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)Declaring errors (implicitly or explicitly) slightly modified the props object
so that the functions called are able to know what your calling function can handle.
That means it only works if you pass or spread your props object!
If your calling function also declares the error, it will JS throw it to you. Then your
function can check the props it was passed to see what its calling function handled.
ST functions will JS throw up the chain as long as the caller knows that error type,
once a props does not mention that error. Then, it will return it. This is exactly
why TypeScript removes it from the return type for this callsite.
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
propsobject 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
Errorinstances - 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
| Authored | Sep 22nd 2022 |
| Updated | Jan 24th 2026 |
| ID | n-rd-stg |