Claude
Skills
Sign in
Back

TypeScript

Included with Lifetime
$97 forever

This skill should be used when the user asks to "write TypeScript", "create a TS module", "implement a TypeScript class", "model this type", "add types", "type this function", "use generics", "use branded types", "use discriminated unions", "fix this TypeScript", "refactor this TS code", "use modern TypeScript", "configure tsconfig", or any task involving writing, reviewing, or improving TypeScript code. Provides best practices for type modeling, modern language features (TypeScript 5.5-5.8), error handling, module patterns, and project configuration. This skill covers TypeScript (.ts/.tsx) files, NOT plain JavaScript with JSDoc.

Productivity

What this skill does


# TypeScript

Best practices for writing modern, type-safe TypeScript. Covers type modeling, modern compiler features (5.5-5.8), error handling, module patterns, and project configuration.

This skill targets `.ts` and `.tsx` files. For plain JavaScript with JSDoc type annotations, use the JavaScript skill instead.

## Project Setup

### tsconfig.json

Enable strict mode in `tsconfig.json` for every TypeScript project:

```json
{
  "compilerOptions": {
    "strict": true,
    "target": "ES2024",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "isolatedModules": true,
    "verbatimModuleSyntax": true,
    "noUncheckedIndexedAccess": true,
    "noUncheckedSideEffectImports": true
  },
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules"]
}
```

Adjust `target`, `module`, and `moduleResolution` to match the project's runtime:
- **Node.js projects:** `"module": "nodenext"`, `"moduleResolution": "nodenext"`
- **Bundler-driven projects:** `"module": "preserve"`, `"moduleResolution": "bundler"`
- **Direct execution (Node.js 22.6+):** Add `"erasableSyntaxOnly": true` to ensure all TS-specific syntax can be stripped without altering runtime behavior. Avoid `enum`, `namespace`, and parameter properties (`constructor(public x: number)`) when this flag is enabled.

### Notable Compiler Options (5.5-5.8)

| Option | Version | Purpose |
|--------|---------|---------|
| `noUncheckedSideEffectImports` | 5.6 | Errors on unresolvable `import "polyfill"` |
| `erasableSyntaxOnly` | 5.8 | Restricts to type-erasable syntax for direct execution |
| `verbatimModuleSyntax` | 5.4 | Enforces explicit `import type` for type-only imports |
| `noUncheckedIndexedAccess` | 4.1 | Adds `undefined` to index signature results |

## Type Modeling

### Prefer Inference Over Annotation

Let TypeScript infer types when the result is unambiguous. Annotate return types on public API surfaces and when inference would be too wide:

```typescript
// Inferred — no annotation needed
const count = items.length;
const doubled = numbers.map(n => n * 2);

// Annotate: public API, inference would be too wide
function parseConfig(raw: string): AppConfig {
  return JSON.parse(raw) as AppConfig;
}
```

### Discriminated Unions

Model variant types using a shared literal discriminant property. Use exhaustiveness checking with `never` to catch unhandled cases:

```typescript
type Result<T> =
  | { kind: 'success'; value: T }
  | { kind: 'error'; error: Error };

function handle<T>(result: Result<T>): T {
  switch (result.kind) {
    case 'success': return result.value;
    case 'error': throw result.error;
    default: {
      const _exhaustive: never = result;
      throw new Error(`Unhandled case: ${_exhaustive}`);
    }
  }
}
```

### The `satisfies` Operator

Use `satisfies` to validate a value conforms to a type while preserving narrower inference. Prefer `satisfies` over `as` for type validation:

```typescript
type Route = { path: string; auth: boolean };
type Routes = Record<string, Route>;

// Type annotation: loses key knowledge
const annotated: Routes = { home: { path: '/', auth: false } };
annotated.home; // Route — no key autocomplete

// satisfies: validates AND preserves literal keys
const checked = {
  home: { path: '/', auth: false },
  dashboard: { path: '/dash', auth: true },
} satisfies Routes;
checked.home; // { path: string; auth: boolean } — keys autocomplete

// as const satisfies: preserves literal values too
const frozen = {
  home: { path: '/', auth: false },
} as const satisfies Routes;
frozen.home.path; // '/' (literal type)
```

### Branded Types

Create nominally distinct types from the same underlying type to prevent accidental interchange:

```typescript
declare const __brand: unique symbol;
type Brand<T, B extends string> = T & { readonly [__brand]: B };

type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;

function createUserId(id: string): UserId {
  if (!id) throw new Error('Invalid user ID');
  return id as UserId;
}

function lookupUser(id: UserId): User { /* ... */ }
lookupUser(createUserId('abc'));  // OK
lookupUser('abc');                // Error: string is not UserId
```

### `NoInfer<T>`

Use `NoInfer` (TS 5.4+) to control which parameter TypeScript infers a generic from:

```typescript
function createFSM<TState extends string>(
  states: TState[],
  initial: NoInfer<TState>  // Must be one of the states, inferred from `states`
) { /* ... */ }

createFSM(['idle', 'running', 'done'], 'idle');     // OK
createFSM(['idle', 'running', 'done'], 'invalid');  // Error
```

### Const Type Parameters

Use `const` type parameters (TS 5.0+) to infer literal types instead of widened types:

```typescript
function createRoutes<const T extends readonly Route[]>(routes: T) {
  return routes;
}
// Infers tuple of literal types, not Route[]
```

For advanced patterns including template literal types, mapped types, conditional types, and complex generics, consult `references/type-patterns.md`.

## Modern Language Features

### Inferred Type Predicates (TS 5.5)

TypeScript 5.5 infers type predicates from function bodies. Manual type guards are no longer needed for simple narrowing:

```typescript
// TS 5.5+: automatic inference, no annotation needed
const validUsers = users.filter(user => user !== null);
// Inferred as User[] (not (User | null)[])
```

Inference requires: a single return statement, no parameter mutation, and a boolean expression tied to a parameter refinement. Truthiness checks (`!!x`) infer predicates for object types but not for numbers (where `0` is falsy).

### Explicit Resource Management (Stage 3)

Use `using` and `await using` for automatic resource cleanup:

```typescript
function readFile(path: string) {
  using handle = openFile(path); // Symbol.dispose called at scope exit
  return handle.read();
}

async function connectDB() {
  await using conn = await pool.getConnection(); // Symbol.asyncDispose called
  return conn.query('SELECT 1');
}
```

Requires `"lib": ["esnext.disposable"]` in tsconfig. Check target runtime support — Chrome 134+, not yet cross-browser.

## Error Handling

### Error Cause Chains

Wrap errors with context using the `cause` option:

```typescript
try {
  const data = JSON.parse(raw);
} catch (err) {
  throw new Error('Failed to parse config', { cause: err });
}
```

### Result Pattern

Prefer discriminated union results over thrown exceptions for expected failure cases:

```typescript
type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

async function fetchUser(id: string): Promise<Result<User>> {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) return { ok: false, error: new Error(`HTTP ${res.status}`) };
  return { ok: true, value: await res.json() };
}
```

Reserve `throw` for truly exceptional / unrecoverable situations. Use `Result` for operations with expected failure modes (network requests, parsing, validation).

## Module Patterns

### ESM Conventions

- Use `"type": "module"` in `package.json`
- Prefer named exports over default exports for discoverability and refactoring
- Use the `"exports"` field to define the package's public API
- Avoid barrel files (`index.ts` re-exporting everything) in application code — they degrade tree-shaking, slow builds, and risk circular dependencies
- Prefer direct imports: `import { thing } from './utils/thing.js'`
- Use `import type` (or `verbatimModuleSyntax`) for type-only imports

### Class Patterns

Use private fields (`#field`) and accessor keywords:

```typescript
class Elevator {
  #currentFloor: number;
  #destination: number | null = null;

  constructor(startFloor: number) {
    this.#currentFloor = startFloor;
  }

  get currentFloor(): number {
    return this.#currentFloor;
  }
}
```

## TypeScript 7 / Compiler Rewrite

The TypeScript compiler is 
Files: 3
Size: 26.4 KB
Complexity: 51/100
Category: Productivity

Related in Productivity