MUST USE when writing or editing TypeScript types, interfaces, generics, type guards, error handling patterns, or tsconfig configuration. Enforces TypeScript 7.x strict-mode best practices and type safety patterns.
3 stars
0 votes
0 copies
5 views
Added August 31, 2026
developmenttypescriptgovueangularnodeapidatabase
Works with
api
Security analysis
A92/100
mediumInstalls packages at runtime which could introduce malicious dependencies
Installs into .claude/skills of the current project.
Are you the author of Typescript Best Practices?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/spardutti-typescript-best-practices)
---
name: typescript-best-practices
category: Foundations
description: "MUST USE when writing or editing TypeScript types, interfaces, generics, type guards, error handling patterns, or tsconfig configuration. Enforces TypeScript 7.x strict-mode best practices and type safety patterns."
tracks: typescript@7.x
metadata:
gate-paths: "**/*.ts, **/*.tsx"
---
# TypeScript Best Practices (TypeScript 7.x)
## Type Design
### Interface for objects, Type for everything else
```typescript
// interface: extendable object shapes
interface Animal { name: string; }
interface Dog extends Animal { breed: string; }
// type: unions, intersections, computed types
type Result = Success | Failure;
type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] };
```
### Discriminated Unions over Optional Properties
```typescript
// BAD: ambiguous states
interface Shape { kind: string; radius?: number; width?: number; height?: number; }
// GOOD: impossible states are unrepresentable
interface Circle { kind: "circle"; radius: number; }
interface Rectangle { kind: "rectangle"; width: number; height: number; }
type Shape = Circle | Rectangle;
```
### Branded Types
```typescript
type Brand<T, B extends string> = T & { readonly __brand: B };
type UserId = Brand<string, "UserId">;
type ProductId = Brand<string, "ProductId">;
function getUser(id: UserId) {}
getUser(createProductId("prod_123")); // ERROR
```
### as const
```typescript
const ROLES = ["admin", "user", "guest"] as const;
type Role = (typeof ROLES)[number]; // "admin" | "user" | "guest"
```
### Utility Types
```typescript
type UserPreview = Pick<User, "id" | "name">;
type CreateUserInput = Omit<User, "id" | "createdAt">;
type UpdateUserInput = Partial<Omit<User, "id">>;
type RolePermissions = Record<User["role"], string[]>;
```
## Type Safety
### Avoid `any` — use `unknown`
```typescript
// BAD
function parseJSON(json: string): any { return JSON.parse(json); }
// GOOD
function parseJSON(json: string): unknown { return JSON.parse(json); }
const data = parseJSON('{}');
if (typeof data === "object" && data !== null && "name" in data) {
console.log(data.name);
}
```
### Type Guards
```typescript
// BAD: type assertion
const user = value as User;
// GOOD: type predicate
function isUser(value: unknown): value is User {
return typeof value === "object" && value !== null && "id" in value && "name" in value;
}
if (isUser(value)) console.log(value.name);
// GOOD: assertion function
function assertIsUser(value: unknown): asserts value is User {
if (!isUser(value)) throw new Error("Expected User");
}
```
### Exhaustive Checks with `never`
```typescript
function assertNever(value: never): never {
throw new Error(`Unexpected value: ${value}`);
}
function getLabel(status: Status): string {
switch (status) {
case "pending": return "Pending";
case "active": return "Active";
case "archived": return "Archived";
default: return assertNever(status); // compile error if case missing
}
}
```
### `satisfies` — validate + preserve inference
```typescript
// BAD: annotation widens, losing literal info
const colors: Record<string, string | [number, number, number]> = { red: [255, 0, 0], green: "#00ff00" };
colors.red.toUpperCase(); // ERROR
// GOOD
const colors = { red: [255, 0, 0], green: "#00ff00" } satisfies Record<string, string | [number, number, number]>;
colors.green.toUpperCase(); // OK
```
### `as const satisfies` — lock literals and validate
```typescript
// Freezes literal types AND checks the shape — the idiom for config objects
const config = {
retries: 3,
mode: "strict",
} as const satisfies AppConfig;
config.mode; // type is "strict", not string — and the shape was checked
```
### Validate External Data (Zod)
```typescript
const UserSchema = z.object({ id: z.number(), name: z.string(), email: z.string().email() });
type User = z.infer<typeof UserSchema>;
async function fetchUser(): Promise<User> {
const data = await fetch("/api/user").then(r => r.json());
return UserSchema.parse(data);
}
```
## Function Patterns
```typescript
// Overloads: only when return type depends on input
function parse(input: string): number;
function parse(input: string[]): number[];
function parse(input: string | string[]): number | number[] {
return Array.isArray(input) ? input.map(Number) : Number(input);
}
// Generic constraints
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; }
// const type parameter: preserve literal inference without caller-side `as const`
function asTuple<const T extends readonly unknown[]>(t: T): T { return t; }
const pair = asTuple(["a", "b"]); // readonly ["a", "b"] — not string[]
// Parameter objects over long param lists
interface CreateUserParams { name: string; email: string; role: "admin" | "user"; }
function createUser(params: CreateUserParams) {}
// Infer return types internally, annotate on exports
export function fetchUsers(): Promise<User[]> { return api.get("/users"); }
```
## Type-Only Imports
With `verbatimModuleSyntax` (see tsconfig), imports used only as types must be marked `type` — they are erased from the JS output, preventing accidental runtime imports and breaking import cycles.
```typescript
// GOOD: type-only import — erased at compile time
import type { User } from "./types";
// GOOD: inline `type` for a mixed import
import { type Role, createUser } from "./users";
```
## Resource Management — `using`
`using` (and `await using`) auto-dispose a resource when the scope exits, even on throw. The resource implements `Symbol.dispose` / `Symbol.asyncDispose`.
```typescript
function openConn(url: string) {
const conn = connect(url);
return { conn, [Symbol.dispose]() { conn.close(); } };
}
function query() {
using db = openConn(DATABASE_URL); // db.conn.close() runs automatically
return db.conn.run("SELECT 1"); // ...even if this throws
}
```
## Common Pitfalls
### Object.keys returns `string[]`
```typescript
// GOOD: type-safe helper
function typedKeys<T extends object>(obj: T): (keyof T)[] {
return Object.keys(obj) as (keyof T)[];
}
```
### Array.filter needs type predicates
```typescript
// BAD: doesn't narrow
const filtered = values.filter(v => v != null); // still (string | null)[]
// GOOD
function nonNullable<T>(value: T): value is NonNullable<T> { return value != null; }
const filtered = values.filter(nonNullable); // string[]
```
### Prefer unions over enums
```typescript
// BAD: numeric enums accept any number
enum Direction { Up, Down }
move(99); // NO ERROR
// GOOD: union types
type Direction = "up" | "down" | "left" | "right";
// GOOD: const object when you need runtime values
const Status = { Active: "active", Inactive: "inactive" } as const;
type Status = (typeof Status)[keyof typeof Status];
```
## Error Handling — Result Pattern
```typescript
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
function ok<T>(value: T): Result<T, never> { return { ok: true, value }; }
function err<E>(error: E): Result<never, E> { return { ok: false, error }; }
type AppError =
| { type: "NOT_FOUND"; resource: string }
| { type: "VALIDATION"; fields: string[] }
| { type: "UNAUTHORIZED"; reason: string };
async function fetchUser(id: string): Promise<Result<User, AppError>> {
const res = await fetch(`/api/users/${id}`);
if (res.status === 404) return err({ type: "NOT_FOUND", resource: "User" });
if (res.status === 401) return err({ type: "UNAUTHORIZED", reason: "Invalid token" });
return ok(UserSchema.parse(await res.json()));
}
```
## Recommended tsconfig.json
```jsonc
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "bundler",
"isolatedModules": true,
"verbatimModuleSyntax": true,
"target": "ES2022",
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"paths": { "@/*": ["./src/*"] }
}
}
```
Under **TypeScript 7.0** (current stable, GA July 8 2026), `tsc` is the **native Go compiler** — a plain `npm i -D typescript` installs it and type-checks ~10× faster; the old `tsgo` / `@typescript/native-preview` preview package is now just `tsc`. `strict`, `module: "esnext"`, and a modern `target` are defaults — the block above keeps them explicit for clarity and older toolchains. `moduleResolution` values `node` / `node10` / `classic` and `baseUrl` remain deprecated; `paths` works without `baseUrl`. Caveat: 7.0 ships no stable programmatic compiler API yet (lands in 7.1, ~Oct 2026), so editor tooling that depends on it — Vue / Svelte / Astro / Angular template type-checking — may need to stay on 6.x until then.
## Rules
1. **Always** enable `strict: true` and `noUncheckedIndexedAccess`
2. **Always** use `unknown` instead of `any` — narrow with type guards
3. **Always** use discriminated unions for multi-shape types
4. **Always** use exhaustive `never` checks in switch statements
5. **Always** validate external data at boundaries (Zod/valibot)
6. **Always** annotate return types on exported functions
7. **Never** use numeric enums — use union types or const objects
8. **Never** use type assertions (`as`) when a type guard is possible
9. **Prefer** `satisfies` over type annotations when you need both validation and inference
10. **Prefer** `as const` for literal arrays and config objects; `as const satisfies T` when you also need a shape check
11. **Prefer** `const` type parameters over caller-side `as const` for literal-preserving generics
12. **Use** `using` / `await using` for resources that need deterministic cleanup
13. **Mark type-only imports** with `import type` — required under `verbatimModuleSyntax`