Skip to main content
For TypeScript teams shipping critical code

soundscript

A stricter subset of TypeScript for the parts of your app where bugs are expensive.

soundscript is a subset of TypeScript, so your existing editor, linter, formatter, and workflow still work. Use .sts files for auth, request parsing, background jobs, workflow logic, and other code where a bad assumption becomes a real incident.

Why soundscript?

It gives TypeScript teams a stricter way to write the code where bugs are painful, without making them abandon the stack they already know.

In plain English, “sound” means the checker stops you from claiming something is safe unless the program has actually proved it.

Fewer production surprises

soundscript blocks a few common shortcuts that tend to hide bugs until the code is already in production.

Clearer code review

If a checked file depends on ordinary TypeScript or JavaScript, that dependency stays visible instead of blending into the import list.

No big rewrite

Start with auth, parsing, background jobs, and other files where one bad assumption can do real damage. Expand from there only if it pays off.

What changes in practice

Here are a few places where .sts is stricter than ordinary TypeScript.

soundscript is a subset of TypeScript, but `.sts` is stricter about imports, assignments, and local guarantees. These are places where TypeScript code often still looks fine right up until it fails at runtime.

Mutable assignment gets stricter

soundscript rejects writable type widenings that let one part of the program change data in a way another part is not prepared for.

TypeScript
const ids: string[] = ['a'];
const values: (string | number)[] = ids;
values.push(1);
soundscript
const ids: string[] = ['a'];
const values: (string | number)[] = [...ids];
values.push(1);

Function parameters stay honest

The classic Animal/Dog callback problem does not get to hide inside assignability. If a function needs a Dog, soundscript does not let it pretend it handles every Animal.

TypeScript
type Animal = {name: string};
type Dog = Animal & {bark(): void};
const trainDog = (dog: Dog) => dog.bark();
let onAnimal: (animal: Animal) => void;
onAnimal = trainDog;
soundscript
type Animal = {name: string};
type Dog = Animal & {bark(): void};
declare function isDog(
animal: Animal,
): animal is Dog;
const trainDog = (dog: Dog) => dog.bark();
const onAnimal = (animal: Animal) => {
if (isDog(animal)) {
trainDog(animal);
}
};

Regular TypeScript stays marked

When a checked file depends on ordinary TypeScript or JavaScript, you mark it so the boundary is obvious in review.

TypeScript
import { loadLegacy } from './legacy.ts';
soundscript
// #[interop]
import { loadLegacy } from './legacy.ts';

Unchecked shortcuts are gone

If a value might be wrong, you have to prove it instead of asserting it away with `as`, `!`, or `any`.

TypeScript
const user = raw as User;
const id = maybeId!;
soundscript
const user = parseUser(raw);
const id = user.id;

Conditions stop guessing

A checked file cannot quietly treat truthy and falsy values as proof. The condition has to say what is actually being ruled out.

TypeScript
if (maybeId) {
loadUser(maybeId);
}
soundscript
if (maybeId !== undefined) {
loadUser(maybeId);
}

Linters can catch some of this. soundscript makes these rules part of `.sts` itself and checks them together with types, imports, and assignment rules.

Read the full comparison for the exact rules in `.sts`, including error handling, narrowing, assignment, and banned constructs.

How it fits

Start with one file, not a rewrite.

Keep the TypeScript and npm setup you already use. Because soundscript is a subset of TypeScript, your editor support, linting, and existing project tooling continue to work while you tighten the files that need extra care.

01

Start with one important file

Pick auth checks, request parsing, workflow code, or a background job where a mistake is costly.

02

Rename it to `.sts`

Run the checker and fix the places where the file relied on unchecked assumptions.

03

Mark imports from regular TS

Use `// #[interop]` when a checked file depends on ordinary TypeScript or JavaScript, then shrink those imports over time.

Read the adoption guide

A checked file around existing payments code

refund-order.sts
// src/refund-order.sts
// #[interop]
import { loadCharge, refundCharge } from '../legacy/payments.ts';

export async function refundOrder(orderId: string): Promise<Refund | undefined> {
const charge = await loadCharge(orderId);

if (charge === undefined) {
return undefined;
}

return refundCharge(charge.id, charge.capturedCents);
}
When you need more depth

Start simple. Go deeper only when the problem calls for it.

Most teams should begin with stricter `.sts` files. When you need more, soundscript also has tools for fixed-width numbers, safer domain models, and repeated patterns.

Evaluate it

Start with the path that matches how your team evaluates tools.

Compare it to TypeScript first, read the adoption guide if rollout is the question, or jump into the quick start if you already know what you want to try.