| name | coding-guidelines-typescript |
| plugin | coding |
| description | Apply and enforce TypeScript-specific coding standards. Use alongside coding-guidelines for any TypeScript file — covers typing, no-any policy, interfaces, abstract classes, async typing, discriminated unions, and pre-commit checks. Trigger on any TypeScript work session, PR review, code generation task, or refactoring. Also trigger when writing TypeScript even if the user doesn't explicitly ask for guidelines — if the user asks you to write a function, add a feature, fix a bug, or review code in a .ts or .tsx file, apply these standards automatically without being asked.
|
TypeScript Coding Guidelines
The goal of these guidelines is to make TypeScript's type system do real work:
catching bugs before runtime, making contracts explicit, and eliminating the
"it works if you pass the right thing" class of errors. Follow them automatically
whenever working in TypeScript — don't wait for the user to ask.
No any
any disables the type checker for that value and everything that touches it.
Use unknown when you don't know the type yet — it forces you to narrow before
using the value, which is the point.
function parse(data: any): any {
return data.value;
}
function parse<T>(data: unknown): T {
if (!isValidShape<T>(data)) throw new Error("Unexpected shape");
return data;
}
Generics over unknown casts: if the shape is known at call time, express it
as a generic parameter so the compiler can verify it end-to-end.
No non-null assertions without a comment
! is a lie you tell the compiler. It suppresses null checks and shifts errors
to runtime. Only use it when you have information the compiler cannot infer, and
always document why on the same line.
const el = document.getElementById("root")!;
const el = document.getElementById("root")!;
When you find yourself reaching for !, first ask whether an earlier narrowing
or a default value would express the intent more clearly.
Explicit return types on exported functions
Return types are part of the public contract. Omitting them means callers
discover the type by reading the implementation — which defeats the purpose of
having an interface. Infer private helpers freely; annotate exports always.
export function getUser(id: string) {
return db.query(`SELECT * FROM users WHERE id = $1`, [id]);
}
export async function getUser(id: string): Promise<User | null> {
return db.query(`SELECT * FROM users WHERE id = $1`, [id]);
}
Interfaces and abstract classes — no duck typing
Duck typing ({ execute(): void }) makes contracts invisible. Define a named
interface for every shape that crosses a boundary (function parameter, return
value, API response). Use abstract class when shared behaviour belongs in the
base — not for shapes that are purely structural.
function run(runner: { execute(): void; name: string }) {
console.log(runner.name);
runner.execute();
}
interface Runner {
name: string;
execute(): void;
}
function run(runner: Runner): void {
console.log(runner.name);
runner.execute();
}
No type assertions without justification
as SomeType tells the compiler "trust me", bypassing all checks. It is
appropriate only when you genuinely have information the compiler cannot infer
(e.g., you know the shape of an API response by contract). Document the reason
inline — if you can't explain it, that's a signal to use a runtime guard instead.
const user = response.data as User;
const user = response.data as User;
function isUser(val: unknown): val is User {
return typeof val === "object" && val !== null && "id" in val;
}
const user = isUser(response.data) ? response.data : null;
Union types and discriminated unions over string flags
Raw string parameters in conditions are fragile — a typo compiles fine and
fails silently at runtime. Use a union literal or const enum to close the set
of valid values at the type level. For objects that can be one of several shapes,
use a discriminated union with a literal kind or type field so TypeScript
can narrow exhaustively.
function exportData(format: string) {
if (format === "pdf") { ... }
}
type ExportFormat = "pdf" | "csv" | "xlsx";
function exportData(format: ExportFormat): Buffer { ... }
type Result<T> =
| { kind: "ok"; value: T }
| { kind: "err"; error: string };
function handle<T>(result: Result<T>): T {
if (result.kind === "ok") return result.value;
throw new Error(result.error);
}
Async functions return typed Promises
Async functions that can fail should make the failure type explicit. Avoid
returning Promise<any> — it infects callers and makes error handling
invisible. Prefer a discriminated union result type over throwing for
recoverable errors.
async function fetchUser(id: string): Promise<any> {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
async function fetchUser(id: string): Promise<User | null> {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) return null;
return res.json() as User;
}
Pre-commit checklist (TypeScript)
Run npm run check before every commit. Then verify:
References
For extended examples and the reasoning behind each rule, see
references/extended-examples.md. Load it when you need to explain a guideline
in depth or handle a case not covered above.