TypeScript coding conventions including type system best practices, strict mode configuration, utility types, error handling, and project organization patterns. Use when writing, reviewing, or refactoring TypeScript code.
// tsconfig.json — non-negotiable strict settings
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"exactOptionalPropertyTypes": true,
"forceConsistentCasingInFileNames": true,
"verbatimModuleSyntax": true
}
}
strict: true EnablesstrictNullChecks — null and undefined are distinct typesstrictFunctionTypes — contravariant function parameter checkingstrictBindCallApply — correct types for , , bindcallapplystrictPropertyInitialization — class properties must be initializednoImplicitAny — no implicit any typesnoImplicitThis — this must have an explicit typealwaysStrict — emit "use strict" in every filestrict mode for convenience*.d.ts)
rather than using any@ts-ignore@ts-expect-error only when a type error is genuinely expected
(e.g., testing invalid inputs) — and add a comment explaining whyas const for literal types when values are fixed// Bad: Too broad
function setStatus(status: string) {}
// Good: Narrow and type-safe
type OrderStatus = 'pending' | 'confirmed' | 'shipped' | 'delivered';
function setStatus(status: OrderStatus) {}
interface for object shapes that may be extended or implementedtype for unions, intersections, mapped types, and utility typesinterface
for public API contracts// Interface: extensible object shape
interface User {
id: string;
name: string;
email: string;
}
// Type: union, intersection, utility
type Result<T> = { success: true; data: T } | { success: false; error: string };
type ReadonlyUser = Readonly<User>;
type UserKeys = keyof User;
type or kind discriminant propertynever to catch unhandled casestype ApiResponse<T> =
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: string };
function handleResponse<T>(response: ApiResponse<T>) {
switch (response.status) {
case 'loading':
return showSpinner();
case 'success':
return renderData(response.data);
case 'error':
return showError(response.error);
default:
// Exhaustive check — compile error if a case is missed
const _exhaustive: never = response;
return _exhaustive;
}
}
| Utility | Purpose |
|---|---|
Partial<T> | Make all properties optional |
Required<T> | Make all properties required |
Readonly<T> | Make all properties readonly |
Pick<T, K> | Select specific properties |
Omit<T, K> | Exclude specific properties |
Record<K, V> | Create an object type with key/value types |
Extract<T, U> | Extract union members matching U |
Exclude<T, U> | Remove union members matching U |
NonNullable<T> | Remove null and undefined |
ReturnType<T> | Get function return type |
Parameters<T> | Get function parameter types as tuple |
extends to specify constraints// Bad: Unconstrained generic
function getProperty<T>(obj: T, key: string) {
return (obj as any)[key]; // unsafe
}
// Good: Constrained generic
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]; // type-safe
}
strictNullChecks (included in strict: true)undefined for optional/missing values,
null for intentional absence! (non-null assertion) in production code
unless the invariant is documented and guaranteed?.) and nullish coalescing (??)// Bad: Non-null assertion hiding potential bugs
const name = user!.name;
// Good: Explicit handling
const name = user?.name ?? 'Unknown';
// Good: Type guard with early return
function processUser(user: User | undefined) {
if (!user) {
throw new Error('User is required');
}
// user is narrowed to User here
return user.name;
}
// Optional property: key may not exist
interface Config {
timeout?: number; // { timeout: 500 } or {}
}
// Explicit undefined: key exists but may have no value
interface Config {
timeout: number | undefined; // { timeout: 500 } or { timeout: undefined }
}
// Bad: Positional parameters
function createUser(
name: string,
email: string,
role: string,
isActive: boolean
) {}
// Good: Named parameters
interface CreateUserParams {
name: string;
email: string;
role: UserRole;
isActive: boolean;
}
function createUser(params: CreateUserParams): User {
// ...
}
function parse(input: string): ParsedText;
function parse(input: Buffer): ParsedBinary;
function parse(input: string | Buffer): ParsedText | ParsedBinary {
// implementation
}
index.ts) at module boundaries only// Define error types with discriminants
class NotFoundError extends Error {
readonly code = 'NOT_FOUND' as const;
constructor(resource: string, id: string) {
super(`${resource} with id ${id} not found`);
this.name = 'NotFoundError';
}
}
class ValidationError extends Error {
readonly code = 'VALIDATION_ERROR' as const;
constructor(
message: string,
public readonly fields: Record<string, string>
) {
super(message);
this.name = 'ValidationError';
}
}
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
function parseConfig(raw: string): Result<Config, ValidationError> {
try {
const parsed = JSON.parse(raw);
// validate...
return { ok: true, value: parsed as Config };
} catch (e) {
return { ok: false, error: new ValidationError('Invalid config', {}) };
}
}
// Caller handles both cases explicitly
const result = parseConfig(input);
if (!result.ok) {
console.error(result.error.message);
return;
}
// result.value is typed as Config here
// Bad: Type assertion (bypasses type checker)
const user = data as User;
// Good: Type guard (runtime verified)
function isUser(data: unknown): data is User {
return (
typeof data === 'object' &&
data !== null &&
'id' in data &&
'name' in data
);
}
if (isUser(data)) {
// data is narrowed to User
}
import { z } from 'zod';
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1).max(100),
email: z.string().email(),
role: z.enum(['admin', 'user', 'viewer']),
});
type User = z.infer<typeof UserSchema>;
// Validates at runtime AND infers TypeScript type
const user = UserSchema.parse(unknownData);
any instead of unknown for values of uncertain type@ts-ignore instead of fixing the type erroras) to silence the compiler
when a type guard is possibleenum with numeric values — prefer string enums
or as const objectsFunction type — use specific function signatures insteadobject type — use Record<string, unknown> or a specific shape