Before / after
What strict mode catches.
Array access — noUncheckedIndexedAccess
Why it matters: without this flag,
items[0] has type Item even when the array might be empty. With it, you get Item | undefined and must handle the empty case explicitly.
without strict
const items: readonly string[] = getItems() // TypeScript accepts this — but items // might be empty at runtime const first: string = items[0] process(first.toUpperCase()) // 💥 runtime
ShotScriptTyping
const items: readonly string[] = getItems() // collapse undefined to null at the access site const first = items[0] ?? null // string | null if (first === null) { return [null, new Error('empty list')] } process(first.toUpperCase()) // ✓ safe
Missing returns — noImplicitReturns
Why it matters: a function that sometimes returns a value and sometimes falls off the end silently returns
undefined. ShotScriptTyping makes this a compile error — every path must return.
without strict
function getLabel(status: string): string { if (status === 'active') { return 'Active' } if (status === 'inactive') { return 'Inactive' } // falls off here — returns undefined // TypeScript accepts it anyway }
ShotScriptTyping
function getLabel(status: string): string { if (status === 'active') { return 'Active' } if (status === 'inactive') { return 'Inactive' } // ✗ compile error: not all code paths // return a value return `Unknown: ${${status}` }
Null propagation — strictNullChecks
Why it matters: without
strictNullChecks, every type implicitly includes null and undefined. With it, nullable values must be explicitly typed and checked before use.
without strict
function getUser(id: number): User | null { return db.find(id) } // TypeScript does not require you // to check — this compiles fine const user = getUser(1) console.log(user.name) // 💥 runtime
ShotScriptTyping
function getUser(id: number): User | null { return db.find(id) } const user = getUser(1) if (user === null) { return [null, new Error('not found')] } // user: User — now safe console.log(user.name) // ✓
Enums — no-enum
Why it matters:
enum emits runtime JavaScript — it's one of the few TypeScript constructs that isn't erased. Numeric enums are also unsound: any number is assignable to them. The replacement is a const object, which is pure type-level and tree-shakeable.
enum — emits runtime JS
enum Status { Active = 'active', Pending = 'pending', Closed = 'closed', } function setStatus(s: Status): void { /* ... */ } setStatus(Status.Active) // ✓ setStatus('active') // ✗ — must use enum ref
const object — erased, tree-shakeable
const Status = { Active: 'active', Pending: 'pending', Closed: 'closed', } as const type Status = typeof Status[keyof typeof Status] function setStatus(s: Status): void { /* ... */ } setStatus(Status.Active) // ✓ setStatus('active') // ✓ — duck-typing accepted
Enforced reference usage (branded types): the
const object pattern accepts raw string literals — setStatus('active') compiles. If you want the enum behaviour of rejecting duck-typed strings, use a branded type. The brand is a compile-time phantom — no runtime cost.
type Brand<T, B extends string> = T & { readonly __brand: B } type Status = Brand<'active' | 'pending' | 'closed', 'Status'> const Status = { Active: 'active' as Status, Pending: 'pending' as Status, Closed: 'closed' as Status, } as const setStatus(Status.Active) // ✓ setStatus('active') // ✗ — rejected, same as enum
What it enables
Install
Install with AI — paste into Claude Code, Cursor, or any AI assistant
Add ShotScriptTyping to this project: run
npm install --save-dev shotscript, then set the extends field in tsconfig.json to "shotscript/tsconfig/shotscript.json". Full ShotScript setup →Manual setup
tsconfig.json
// tsconfig.json { "extends": "shotscript/tsconfig/shotscript.json" }