TypeScript Coding Standards
The full reference for writing TypeScript at Finperiti: strictness, type modelling, generics, runtime validation at boundaries, async, modules, and the discipline that keeps the types meaningful. TypeScript only pays off if the types are accurate and honest. The worst mistakes are any, casts, and skipping runtime validation. Builds on JavaScript Coding Standards (all of which applies) and pairs with React Coding Standards.
We write TypeScript in strict mode and treat type errors as build failures. The whole value is type accuracy. A codebase full of any, as, and @ts-ignore pays the cost of types with none of the safety. Model data precisely, validate it at the edges, and let the compiler act as a reviewer that never tires.
Prettier and ESLint (with the TypeScript and import plugins) enforce formatting and many rules automatically. This page explains the reasoning behind them and the conventions tools cannot enforce.
Strictness — the foundation
- AlwaysKeep
stricton (includingstrictNullChecks,noImplicitAny, andnoUncheckedIndexedAccesswhere feasible) and treat type errors as build failures. Never weaken the config to silence errors. - DoTurn on strict lint rules and follow them (no-floating-promises, no-explicit-any, consistent-type-imports). Fix warnings rather than disabling them.
- Avoid
any. It turns off type-checking for everything it touches. Useunknownand narrow it, or model the real type. - AvoidType assertions (
as) and non-null (!), except where you truly know more than the compiler and can justify it. - NeverUse
@ts-ignoreor@ts-expect-errorto hide a real type problem; fix the type. If a suppression is truly unavoidable, add a comment saying exactly why.
Modelling types
- DoMake illegal states impossible to represent with union and discriminated-union types (a
kindortypetag), so the compiler forces you to handle every case. - DoPrefer precise types: literal unions (
'active' | 'closed') overstring, and specific shapes overobjectorRecord<string, any>. - DoUse
readonlyandas constfor immutable data. Mark arrays and props readonly where they should not change. - DoUse utility types (
Partial,Pick,Omit,Readonly,Record) to derive shapes instead of redefining them. - DoModel optionality on purpose. A missing field (
?) and an explicitnullshould mean different things. - AvoidOver-broad types (
any,object, untyped index signatures) and string-based APIs where a union or branded type is clearer.
type Result = { kind: 'ok'; value: Customer } | { kind: 'error'; message: string };
function render(r: Result) {
switch (r.kind) { // compiler errors if a case is missed
case 'ok': return show(r.value);
case 'error': return showError(r.message);
}
}
The union makes every state explicit, and the compiler forces you to handle each one. Illegal states cannot be represented.
interface vs type, enums
- DoUse
interfacefor object shapes you may extend or implement, andtypefor unions, intersections, mapped, and aliased types. Be consistent within a file. - DoIn most cases prefer literal-union types or
as constobjects overenum(lighter and safer). Use real enums only when you need their specific behaviour. - DoFor variant data, prefer discriminated unions over objects with many optional fields, so each variant carries exactly its own fields.
Boundaries & runtime safety
- AlwaysValidate external data (API responses, form input, config, query params, message payloads) at runtime. TypeScript types are erased and do not check incoming data (see Trust Boundaries).
- DoUse a schema or validation library (for example zod) at boundaries and infer the TypeScript type from the schema, so one definition drives both the type and the validation.
- AvoidCasting untrusted JSON straight to a type (
data as Customer). It tells the compiler something false, and it crashes at runtime when the data differs. - DoHandle
nullandundefinedwith?.and??. Narrowunknownbefore you use it.
async function load(id: string): Promise<any> {
const res = await fetch(`/api/customers/${id}`);
return res.json() as Customer; // unvalidated lie to the compiler
}
An any return type throws away type safety in all the code that follows, and casting the parsed JSON to Customer means a wrong shape crashes at runtime with no warning. The types look nice but protect nothing.
const Customer = z.object({ id: z.string(), status: z.enum(['active','closed']) });
type Customer = z.infer<typeof Customer>;
async function load(id: string): Promise<Customer> {
const res = await fetch(`/api/customers/${id}`);
return Customer.parse(await res.json()); // validated at the boundary
}
One schema drives both the runtime check and the static type, so bad data is caught at the edge and the type truly matches reality.
Functions & generics
- DoGive explicit types to function parameters, to the return types of exported or public functions, and at module boundaries. Let inference handle obvious locals.
- DoKeep generics readable and constrained (
<T extends ...>) with meaningful names. Do not build hard-to-read type puzzles; clear beats clever. - DoPrefer pure functions and immutable inputs. Type async functions as
Promise<T>and never leave a floating promise; await it or handle it. - DoUse function overloads or unions for genuinely different call shapes, not
any.
Classes & objects
- DoPrefer plain objects, types, and functions. Use classes when you need instances with behaviour and state, and keep them small.
- DoUse access modifiers (
private,readonly) and parameter properties for short constructors. Do not leak mutable internals. - DoDepend on interfaces and abstractions, and inject dependencies so code is testable (see Inversion of Control).
- AvoidDeep inheritance. Prefer composition.
Modules & structure
- DoUse ES modules. Prefer named exports over default exports; they are easier to find and refactor.
- DoOrganise by feature, keep modules focused, and use configured path aliases instead of long
../../../chains. - DoUse
import typefor type-only imports, so they are erased and do not cause runtime cycles. - DoKeep each module's public surface small and deliberate (see Backward Compatibility).
Async & errors
- AlwaysNever leave a floating promise. Always
awaitit or handle it. An unhandled rejection is a silent failure, and lint should flag it. - DoUse
async/awaitover chains and callbacks. UsePromise.allfor independent concurrent work, and handle partial failure on purpose. - DoType caught errors as
unknownand narrow before use. Do not assume a caught value is anError(see Error Handling). - DoModel expected failure as data (a Result or union) where that reads better than throwing.
Naming & JavaScript baseline
- DoPascalCase for types, interfaces, enums, and components; camelCase for variables and functions; UPPER_SNAKE for true constants. Name for intent (see Coding Standards & Style).
- DoEverything in JavaScript Coding Standards applies:
constby default, novar, strict equality, immutability, and noeval. - DoKeep secrets and other users' data out of client-side code. The bundle is fully readable (see Web & Frontend Security).
Self-review checklist
- AskIs there any
any,as,!, or@ts-ignoreI could replace with an honest type? - AskIs external or untrusted data validated at runtime, not just cast?
- AskDo the types make illegal states hard to represent (unions, readonly, literals)?
- AskAre there floating promises or mishandled null/undefined? Do public functions have explicit types?
any or casts, or skip runtime validation at boundaries. Strict, honest, well-modelled types plus boundary validation turn the compiler into a tireless reviewer. That is the safety net a fast-moving, junior team needs most.