
Mastering Typescript
- 676 installs
- 27 repo stars
- Updated January 1, 2026
- spillwavesolutions/mastering-typescript-skill
mastering-typescript is an agent skill that enforces strict TypeScript and ESLint 9 flat-config patterns for developers who want typed code with fewer runtime surprises and rigorous lint coverage.
About
mastering-typescript is a spillwavesolutions skill centered on strict TypeScript engineering with ESLint 9+ flat configuration. It provides an eslint.config.js template importing @eslint/js and typescript-eslint, enabling eslint.configs.recommended plus tseslint.configs.strictTypeChecked and tseslint.configs.stylisticTypeChecked bundles. Parser options use projectService with tsconfigRootDir set via import.meta.dirname for accurate type-aware linting across monorepos and modern ESM projects. Custom rule guidance includes conventions such as allowing unused variables only when prefixed with underscores. Developers reach for mastering-typescript when migrating from legacy .eslintrc files, tightening CI lint gates, or aligning agent-generated TypeScript with strict compiler and ESLint policies. The skill targets SaaS, API, and CLI TypeScript codebases where static analysis should catch nullability, unsafe any usage, and style drift before merge.
- Drop-in eslint.config.js for ESLint 9+ using typescript-eslint flat config with strictTypeChecked and stylisticTypeCheck
- Parser uses projectService and tsconfigRootDir for accurate type-aware linting without brittle project paths
- Enforces consistent type-imports (inline-type-imports) for better tree-shaking
- Blocks floating and misused promises, await-thenable mistakes, and nudges nullish coalescing and optional chaining
- Unused bindings allowed only with underscore prefix via argsIgnorePattern and varsIgnorePattern
Mastering Typescript by the numbers
- 676 all-time installs (skills.sh)
- +16 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #195 of 1,352 Code Review & Quality skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/spillwavesolutions/mastering-typescript-skill --skill mastering-typescriptAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 676 |
|---|---|
| repo stars | ★ 27 |
| Security audit | 3 / 3 scanners passed |
| Last updated | January 1, 2026 |
| Repository | spillwavesolutions/mastering-typescript-skill ↗ |
How do you configure strict TypeScript ESLint 9 flat config?
Apply strict TypeScript and ESLint 9 flat-config patterns so developers ship typed code with fewer runtime surprises.
Who is it for?
TypeScript developers adopting ESLint 9 flat config who need strict type-checked and stylistic rules enforced in CI.
Skip if: JavaScript-only codebases without TypeScript, teams staying on legacy .eslintrc without migration plans, or quick prototypes skipping strict typing.
When should I use this skill?
A developer asks for ESLint 9 flat config, typescript-eslint strict rules, eslint.config.js setup, or stricter TypeScript lint CI gates.
What you get
eslint.config.js flat configs, strictTypeChecked rule sets, and projectService-backed type-aware lint pipelines.
- eslint.config.js flat config
- Strict type-aware lint rule set
Files
Mastering Modern TypeScript
Build enterprise-grade, type-safe applications with TypeScript 5.9+.
Compatibility: TypeScript 5.9+, Node.js 22 LTS, Vite 7, NestJS 11, React 19
Quick Start
# Initialize TypeScript project with ESM
pnpm create vite@latest my-app --template vanilla-ts
cd my-app && pnpm install
# Configure strict TypeScript
cat > tsconfig.json << 'EOF'
{
"compilerOptions": {
"target": "ES2024",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}
EOFWhen to Use This Skill
Use when:
- Building type-safe React, NestJS, or Node.js applications
- Migrating JavaScript codebases to TypeScript
- Implementing advanced type patterns (generics, mapped types, conditional types)
- Configuring modern TypeScript toolchains (Vite, pnpm, ESLint)
- Designing type-safe API contracts with Zod validation
- Comparing TypeScript approaches with Java or Python
Project Setup Checklist
Before starting any TypeScript project:
- [ ] Use pnpm for package management (faster, disk-efficient)
- [ ] Configure ESM-first (type: "module" in package.json)
- [ ] Enable strict mode in tsconfig.json
- [ ] Set up ESLint with @typescript-eslint
- [ ] Add Prettier for consistent formatting
- [ ] Configure Vitest for testingType System Quick Reference
Primitive Types
const name: string = "Alice";
const age: number = 30;
const active: boolean = true;
const id: bigint = 9007199254740991n;
const key: symbol = Symbol("unique");Union and Intersection Types
// Union: value can be one of several types
type Status = "pending" | "approved" | "rejected";
// Intersection: value must satisfy all types
type Employee = Person & { employeeId: string };
// Discriminated union for type-safe handling
type Result<T> =
| { success: true; data: T }
| { success: false; error: string };
function handleResult<T>(result: Result<T>): T | null {
if (result.success) {
return result.data; // TypeScript knows data exists here
}
console.error(result.error);
return null;
}Type Guards
// typeof guard
function process(value: string | number): string {
if (typeof value === "string") {
return value.toUpperCase();
}
return value.toFixed(2);
}
// Custom type guard
interface User { type: "user"; name: string }
interface Admin { type: "admin"; permissions: string[] }
function isAdmin(person: User | Admin): person is Admin {
return person.type === "admin";
}The satisfies Operator (TS 5.0+)
Validate type conformance while preserving inference:
// Problem: Type assertion loses specific type info
const colors1 = {
red: "#ff0000",
green: "#00ff00"
} as Record<string, string>;
colors1.red.toUpperCase(); // OK, but red could be undefined
// Solution: satisfies preserves literal types
const colors2 = {
red: "#ff0000",
green: "#00ff00"
} satisfies Record<string, string>;
colors2.red.toUpperCase(); // OK, and TypeScript knows red existsGenerics Patterns
Basic Generic Function
function first<T>(items: T[]): T | undefined {
return items[0];
}
const num = first([1, 2, 3]); // number | undefined
const str = first(["a", "b"]); // string | undefinedConstrained Generics
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(item: T): T {
console.log(item.length);
return item;
}
logLength("hello"); // OK: string has length
logLength([1, 2, 3]); // OK: array has length
logLength(42); // Error: number has no lengthGeneric API Response Wrapper
interface ApiResponse<T> {
data: T;
status: number;
timestamp: Date;
}
async function fetchUser(id: string): Promise<ApiResponse<User>> {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
return {
data,
status: response.status,
timestamp: new Date()
};
}Utility Types Reference
| Type | Purpose | Example |
|---|---|---|
Partial<T> | All properties optional | Partial<User> |
Required<T> | All properties required | Required<Config> |
Pick<T, K> | Select specific properties | `Pick<User, "id" \ |
Omit<T, K> | Exclude specific properties | Omit<User, "password"> |
Record<K, V> | Object with typed keys/values | Record<string, number> |
ReturnType<F> | Extract function return type | ReturnType<typeof fn> |
Parameters<F> | Extract function parameters | Parameters<typeof fn> |
Awaited<T> | Unwrap Promise type | Awaited<Promise<User>> |
Conditional Types
// Basic conditional type
type IsString<T> = T extends string ? true : false;
// Extract array element type
type ArrayElement<T> = T extends (infer E)[] ? E : never;
type Numbers = ArrayElement<number[]>; // number
type Strings = ArrayElement<string[]>; // string
// Practical: Extract Promise result type
type UnwrapPromise<T> = T extends Promise<infer R> ? R : T;Mapped Types
// Make all properties readonly
type Immutable<T> = {
readonly [K in keyof T]: T[K];
};
// Make all properties nullable
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
// Create getter functions for each property
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person { name: string; age: number }
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }Framework Integration
React with TypeScript
// Typed functional component
interface ButtonProps {
label: string;
onClick: () => void;
variant?: "primary" | "secondary";
}
const Button: React.FC<ButtonProps> = ({ label, onClick, variant = "primary" }) => (
<button className={variant} onClick={onClick}>
{label}
</button>
);
// Typed hooks
const [count, setCount] = useState<number>(0);
const userRef = useRef<HTMLInputElement>(null);NestJS with TypeScript
// Type-safe DTO with class-validator
import { IsString, IsEmail, MinLength } from 'class-validator';
class CreateUserDto {
@IsString()
@MinLength(2)
name: string;
@IsEmail()
email: string;
}
// Or with Zod (modern approach)
import { z } from 'zod';
const CreateUserSchema = z.object({
name: z.string().min(2),
email: z.string().email()
});
type CreateUserDto = z.infer<typeof CreateUserSchema>;See react-integration.md and nestjs-integration.md for detailed patterns.
Validation with Zod
import { z } from 'zod';
// Define schema
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1).max(100),
email: z.string().email(),
role: z.enum(["user", "admin", "moderator"]),
createdAt: z.coerce.date()
});
// Infer TypeScript type from schema
type User = z.infer<typeof UserSchema>;
// Validate at runtime
function parseUser(data: unknown): User {
return UserSchema.parse(data); // Throws ZodError if invalid
}
// Safe parsing (returns result object)
const result = UserSchema.safeParse(data);
if (result.success) {
console.log(result.data); // Typed as User
} else {
console.error(result.error.issues);
}Modern Toolchain (2025)
| Tool | Version | Purpose |
|---|---|---|
| TypeScript | 5.9+ | Type checking and compilation |
| Node.js | 22 LTS | Runtime environment |
| Vite | 7.x | Build tool and dev server |
| pnpm | 9.x | Package manager |
| ESLint | 9.x | Linting with flat config |
| Vitest | 3.x | Testing framework |
| Prettier | 3.x | Code formatting |
ESLint Flat Config (ESLint 9+)
// eslint.config.js
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.strictTypeChecked,
{
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
}
);Migration Strategies
Incremental Migration
1. Add allowJs: true and checkJs: false to tsconfig.json 2. Rename files from .js to .ts one at a time 3. Add type annotations gradually 4. Enable stricter options incrementally
JSDoc for Gradual Typing
// Before full migration, use JSDoc
/**
* @param {string} name
* @param {number} age
* @returns {User}
*/
function createUser(name, age) {
return { name, age };
}See enterprise-patterns.md for comprehensive migration guides.
Common Mistakes
| Mistake | Problem | Fix |
|---|---|---|
Using any liberally | Defeats type safety | Use unknown and narrow |
| Ignoring strict mode | Misses null/undefined bugs | Enable all strict options |
Type assertions (as) | Can hide type errors | Use satisfies or guards |
| Enum for simple unions | Generates runtime code | Use literal unions instead |
| Not validating API data | Runtime type mismatches | Use Zod at boundaries |
Cross-Language Comparison
| Feature | TypeScript | Java | Python |
|---|---|---|---|
| Type System | Structural | Nominal | Gradual (duck typing) |
| Nullability | Explicit (`T \ | null`) | @Nullable annotations |
| Generics | Type-level, erased | Type-level, erased | Runtime via typing |
| Interfaces | Structural matching | Must implement | Protocol (3.8+) |
| Enums | Avoid (use unions) | First-class | Enum class |
Reference Files
- type-system.md — Complete type system guide
- generics.md — Advanced generics patterns
- enterprise-patterns.md — Error handling, validation, architecture
- react-integration.md — React + TypeScript patterns
- nestjs-integration.md — NestJS API development
- toolchain.md — Modern build tools configuration
Assets
- tsconfig-template.json — Strict enterprise config
- eslint-template.js — ESLint 9 flat config
Scripts
- validate-setup.sh — Verify TypeScript environment
// eslint.config.js - ESLint 9+ Flat Config for TypeScript
// Copy this file to your project root as eslint.config.js
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config(
// Base ESLint recommendations
eslint.configs.recommended,
// TypeScript strict type-checking
...tseslint.configs.strictTypeChecked,
...tseslint.configs.stylisticTypeChecked,
// TypeScript parser configuration
{
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname
}
}
},
// Custom TypeScript rules
{
rules: {
// Allow unused vars with underscore prefix
'@typescript-eslint/no-unused-vars': ['error', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_'
}],
// Enforce type imports for better tree-shaking
'@typescript-eslint/consistent-type-imports': ['error', {
prefer: 'type-imports',
fixStyle: 'inline-type-imports'
}],
// Prevent unhandled promises
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-misused-promises': 'error',
// Prevent awaiting non-promises
'@typescript-eslint/await-thenable': 'error',
// Prefer nullish coalescing
'@typescript-eslint/prefer-nullish-coalescing': 'error',
// Prefer optional chaining
'@typescript-eslint/prefer-optional-chain': 'error',
// Consistent type assertions
'@typescript-eslint/consistent-type-assertions': ['error', {
assertionStyle: 'as',
objectLiteralTypeAssertions: 'never'
}],
// Naming conventions
'@typescript-eslint/naming-convention': [
'error',
{
selector: 'interface',
format: ['PascalCase']
},
{
selector: 'typeAlias',
format: ['PascalCase']
},
{
selector: 'enum',
format: ['PascalCase']
}
]
}
},
// Ignore patterns
{
ignores: [
'dist/**',
'build/**',
'node_modules/**',
'coverage/**',
'*.config.js',
'*.config.ts'
]
}
);
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
// Language and Environment
"target": "ES2024",
"lib": ["ES2024"],
"module": "ESNext",
"moduleResolution": "bundler",
// Strict Type Checking (Enterprise-Grade)
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true,
// Module Handling
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
// Output
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
// Path Aliases (adjust as needed)
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
// Performance
"skipLibCheck": true,
"incremental": true,
"tsBuildInfoFile": "./node_modules/.cache/tsbuildinfo"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
}
Enterprise Patterns Reference
Load when: User asks about error handling, validation, project architecture, migration strategies, or large-scale TypeScript patterns.
Proven patterns for building maintainable TypeScript applications.
Contents
---
Error Handling
Result Type Pattern
Instead of throwing exceptions, return typed results:
// Define Result type
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
// Helper functions
function ok<T>(data: T): Result<T, never> {
return { success: true, data };
}
function err<E>(error: E): Result<never, E> {
return { success: false, error };
}
// Usage
interface ValidationError {
field: string;
message: string;
}
function parseEmail(input: string): Result<string, ValidationError> {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(input)) {
return err({ field: "email", message: "Invalid email format" });
}
return ok(input.toLowerCase());
}
// Consuming Result
const result = parseEmail(userInput);
if (result.success) {
console.log(`Valid email: ${result.data}`);
} else {
console.error(`Error in ${result.error.field}: ${result.error.message}`);
}Typed Error Classes
// Base application error
abstract class AppError extends Error {
abstract readonly code: string;
abstract readonly statusCode: number;
constructor(message: string) {
super(message);
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}
// Specific error types
class NotFoundError extends AppError {
readonly code = "NOT_FOUND";
readonly statusCode = 404;
constructor(resource: string, id: string) {
super(`${resource} with id ${id} not found`);
}
}
class ValidationError extends AppError {
readonly code = "VALIDATION_ERROR";
readonly statusCode = 400;
constructor(
message: string,
public readonly fields: Record<string, string[]>
) {
super(message);
}
}
class UnauthorizedError extends AppError {
readonly code = "UNAUTHORIZED";
readonly statusCode = 401;
constructor(message = "Authentication required") {
super(message);
}
}
// Type guard for app errors
function isAppError(error: unknown): error is AppError {
return error instanceof AppError;
}
// Error handler
function handleError(error: unknown): { status: number; body: object } {
if (isAppError(error)) {
return {
status: error.statusCode,
body: {
code: error.code,
message: error.message,
...(error instanceof ValidationError && { fields: error.fields })
}
};
}
console.error("Unexpected error:", error);
return {
status: 500,
body: { code: "INTERNAL_ERROR", message: "Internal server error" }
};
}---
Validation Patterns
Zod Schema Validation
import { z } from 'zod';
// Define schemas
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().min(0).max(150).optional(),
role: z.enum(["user", "admin", "moderator"]),
metadata: z.record(z.string()).optional()
});
// Infer TypeScript type
type User = z.infer<typeof UserSchema>;
// Create DTO schemas
const CreateUserSchema = UserSchema.omit({ id: true });
type CreateUserDto = z.infer<typeof CreateUserSchema>;
const UpdateUserSchema = UserSchema.partial().omit({ id: true });
type UpdateUserDto = z.infer<typeof UpdateUserSchema>;
// Validation functions
function validateCreateUser(data: unknown): Result<CreateUserDto, z.ZodError> {
const result = CreateUserSchema.safeParse(data);
if (result.success) {
return ok(result.data);
}
return err(result.error);
}
// Transform Zod errors to user-friendly format
function formatZodError(error: z.ZodError): Record<string, string[]> {
const formatted: Record<string, string[]> = {};
for (const issue of error.issues) {
const path = issue.path.join(".");
if (!formatted[path]) {
formatted[path] = [];
}
formatted[path].push(issue.message);
}
return formatted;
}Branded Types for Validation
// Branded/Nominal types
declare const EmailBrand: unique symbol;
type Email = string & { readonly [EmailBrand]: true };
declare const UserIdBrand: unique symbol;
type UserId = string & { readonly [UserIdBrand]: true };
// Validation functions that return branded types
function validateEmail(input: string): Email {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(input)) {
throw new ValidationError("Invalid email", { email: ["Invalid format"] });
}
return input as Email;
}
function validateUserId(input: string): UserId {
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
if (!uuidRegex.test(input)) {
throw new ValidationError("Invalid user ID", { id: ["Must be UUID"] });
}
return input as UserId;
}
// Usage: Functions require validated types
function sendEmail(to: Email, subject: string): void {
// to is guaranteed to be valid email
}
function getUser(id: UserId): Promise<User> {
// id is guaranteed to be valid UUID
}
// Compiler enforces validation
sendEmail("invalid", "Hello"); // Error: string not assignable to Email
sendEmail(validateEmail("a@b.com"), "Hello"); // OK---
Project Organization
Feature-Based Structure
src/
├── features/
│ ├── users/
│ │ ├── index.ts # Public exports (barrel)
│ │ ├── user.types.ts # Types and interfaces
│ │ ├── user.schema.ts # Zod schemas
│ │ ├── user.service.ts # Business logic
│ │ ├── user.repository.ts # Data access
│ │ ├── user.controller.ts # HTTP handlers
│ │ └── __tests__/
│ │ ├── user.service.test.ts
│ │ └── user.controller.test.ts
│ ├── auth/
│ │ ├── index.ts
│ │ ├── auth.types.ts
│ │ └── ...
│ └── posts/
│ └── ...
├── shared/
│ ├── types/
│ │ ├── result.ts
│ │ └── pagination.ts
│ ├── utils/
│ │ ├── validation.ts
│ │ └── date.ts
│ └── errors/
│ └── app-error.ts
├── infrastructure/
│ ├── database/
│ │ └── client.ts
│ ├── cache/
│ │ └── redis.ts
│ └── logging/
│ └── logger.ts
└── config/
├── index.ts
└── env.tsBarrel Exports
// features/users/index.ts
export type { User, CreateUserDto, UpdateUserDto } from './user.types';
export { UserSchema, CreateUserSchema } from './user.schema';
export { UserService } from './user.service';
export { UserController } from './user.controller';
// Don't export repository (internal detail)
// Don't export internal helper functions
// Usage in other modules
import { User, UserService } from '@/features/users';Path Aliases
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@features/*": ["src/features/*"],
"@shared/*": ["src/shared/*"],
"@config/*": ["src/config/*"]
}
}
}---
Migration Strategies
Incremental Migration from JavaScript
Phase 1: Enable TypeScript alongside JavaScript
// tsconfig.json
{
"compilerOptions": {
"allowJs": true,
"checkJs": false,
"outDir": "./dist",
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": false,
"noImplicitAny": false
},
"include": ["src/**/*"]
}Phase 2: Rename files gradually
# Convert one file at a time
mv src/utils/helpers.js src/utils/helpers.ts
# Add minimal type annotations
# Fix any type errors
# Run tests to verifyPhase 3: Enable stricter checks incrementally
// Progression of strict options
{
"compilerOptions": {
// Step 1: Basic strictness
"noImplicitAny": true,
// Step 2: Null safety
"strictNullChecks": true,
// Step 3: Full strict mode
"strict": true,
// Step 4: Extra safety (optional)
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true
}
}JSDoc for Gradual Typing
// Before full migration, use JSDoc
/**
* @typedef {Object} User
* @property {string} id
* @property {string} name
* @property {string} email
*/
/**
* Find user by ID
* @param {string} id - User ID
* @returns {Promise<User | null>}
*/
async function findUser(id) {
// implementation
}
/**
* @template T
* @param {T[]} items
* @returns {T | undefined}
*/
function first(items) {
return items[0];
}CommonJS to ESM Migration
// package.json
{
"type": "module"
}// Before (CommonJS)
const express = require('express');
const { UserService } = require('./user.service');
module.exports = { router };
// After (ESM)
import express from 'express';
import { UserService } from './user.service.js'; // Note .js extension
export { router };---
Security Patterns
Input Sanitization
import { z } from 'zod';
import DOMPurify from 'isomorphic-dompurify';
// Sanitized string schema
const SanitizedString = z.string().transform((val) => {
return DOMPurify.sanitize(val.trim());
});
// HTML content schema (for rich text)
const HtmlContentSchema = z.string().transform((val) => {
return DOMPurify.sanitize(val, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
ALLOWED_ATTR: ['href', 'target']
});
});
// SQL-safe identifier
const SafeIdentifierSchema = z.string().regex(
/^[a-zA-Z_][a-zA-Z0-9_]*$/,
"Invalid identifier"
);Type-Safe Environment Variables
import { z } from 'zod';
const EnvSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']),
PORT: z.coerce.number().default(3000),
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
REDIS_URL: z.string().url().optional()
});
// Validate on startup
function loadEnv() {
const result = EnvSchema.safeParse(process.env);
if (!result.success) {
console.error('Invalid environment variables:');
console.error(result.error.format());
process.exit(1);
}
return result.data;
}
export const env = loadEnv();
// Usage: fully typed
env.PORT; // number
env.NODE_ENV; // "development" | "production" | "test"
env.REDIS_URL; // string | undefinedSecure API Response Types
// Never expose internal fields
interface InternalUser {
id: string;
name: string;
email: string;
passwordHash: string;
internalNotes: string;
}
// Public API response (Pick only safe fields)
type PublicUser = Pick<InternalUser, 'id' | 'name' | 'email'>;
// Or explicitly define
interface UserResponse {
id: string;
name: string;
email: string;
}
// Transform function
function toPublicUser(user: InternalUser): UserResponse {
return {
id: user.id,
name: user.name,
email: user.email
};
}Rate Limiting Types
interface RateLimitConfig {
windowMs: number;
maxRequests: number;
}
interface RateLimitResult {
allowed: boolean;
remaining: number;
resetAt: Date;
}
const rateLimits: Record<string, RateLimitConfig> = {
api: { windowMs: 60000, maxRequests: 100 },
auth: { windowMs: 300000, maxRequests: 5 },
upload: { windowMs: 3600000, maxRequests: 10 }
} as const satisfies Record<string, RateLimitConfig>;Generics Reference
Load when: User asks about generics, mapped types, conditional types, template literal types, or reusable type patterns.
Advanced generics and type-level programming patterns.
Contents
- Generic Fundamentals
- Generic Constraints
- Mapped Types
- Conditional Types
- Template Literal Types
- Variadic Tuple Types
---
Generic Fundamentals
Basic Generic Function
// Type parameter T can be any type
function identity<T>(value: T): T {
return value;
}
const str = identity("hello"); // string
const num = identity(42); // number
const obj = identity({ x: 1 }); // { x: number }
// Explicit type argument (rarely needed)
const explicit = identity<string>("hello");Generic Interfaces
interface Container<T> {
value: T;
getValue(): T;
setValue(value: T): void;
}
interface Repository<T, ID = string> {
findById(id: ID): Promise<T | null>;
findAll(): Promise<T[]>;
save(entity: T): Promise<T>;
delete(id: ID): Promise<boolean>;
}
// Implementation
class UserRepository implements Repository<User> {
async findById(id: string): Promise<User | null> {
// implementation
}
// ... other methods
}Generic Classes
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
isEmpty(): boolean {
return this.items.length === 0;
}
}
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
const top = numberStack.pop(); // number | undefined---
Generic Constraints
extends Constraint
// T must have a length property
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(item: T): T {
console.log(`Length: ${item.length}`);
return item;
}
logLength("hello"); // OK: string has length
logLength([1, 2, 3]); // OK: array has length
logLength({ length: 10 }); // OK: object has length
logLength(42); // Error: number has no lengthkeyof Constraint
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
interface Person {
name: string;
age: number;
}
const person: Person = { name: "Alice", age: 30 };
const name = getProperty(person, "name"); // string
const age = getProperty(person, "age"); // number
const bad = getProperty(person, "email"); // Error: "email" not in PersonMultiple Constraints
interface Printable {
print(): void;
}
interface Loggable {
log(): string;
}
// T must satisfy both interfaces
function process<T extends Printable & Loggable>(item: T): void {
item.print();
console.log(item.log());
}Default Type Parameters
interface ApiResponse<T = unknown, E = Error> {
data?: T;
error?: E;
status: number;
}
// Uses defaults
const response1: ApiResponse = { status: 200 };
// Override data type only
const response2: ApiResponse<User> = { data: user, status: 200 };
// Override both
const response3: ApiResponse<User, ValidationError> = {
error: new ValidationError(),
status: 400
};---
Mapped Types
Basic Mapped Types
// Transform all properties to optional
type Partial<T> = {
[K in keyof T]?: T[K];
};
// Transform all properties to required
type Required<T> = {
[K in keyof T]-?: T[K];
};
// Transform all properties to readonly
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
// Remove readonly modifier
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};Practical Mapped Types
// Make all properties nullable
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
// Make all properties async getters
type AsyncGetters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => Promise<T[K]>;
};
interface User {
name: string;
email: string;
}
type UserGetters = AsyncGetters<User>;
// {
// getName: () => Promise<string>;
// getEmail: () => Promise<string>;
// }Key Remapping (as clause)
// Filter keys by type
type FilterByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
interface Mixed {
name: string;
age: number;
active: boolean;
score: number;
}
type StringProps = FilterByType<Mixed, string>;
// { name: string }
type NumberProps = FilterByType<Mixed, number>;
// { age: number; score: number }
// Prefix all keys
type Prefixed<T, P extends string> = {
[K in keyof T as `${P}${Capitalize<string & K>}`]: T[K];
};
type PrefixedUser = Prefixed<User, "user">;
// { userName: string; userEmail: string }---
Conditional Types
Basic Conditional Types
// T extends U ? X : Y
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
type C = IsString<"hello">; // true
// Practical: Extract non-nullable type
type NonNullable<T> = T extends null | undefined ? never : T;
type D = NonNullable<string | null>; // string
type E = NonNullable<number | undefined>; // numberDistributive Conditional Types
// Conditional types distribute over unions
type ToArray<T> = T extends unknown ? T[] : never;
type StringOrNumberArray = ToArray<string | number>;
// string[] | number[] (not (string | number)[])
// Prevent distribution with tuple
type ToArrayNonDist<T> = [T] extends [unknown] ? T[] : never;
type Mixed = ToArrayNonDist<string | number>;
// (string | number)[]infer Keyword
// Extract return type
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type FnReturn = ReturnType<() => string>; // string
// Extract array element type
type ArrayElement<T> = T extends (infer E)[] ? E : never;
type Element = ArrayElement<number[]>; // number
// Extract Promise result
type Awaited<T> = T extends Promise<infer R> ? Awaited<R> : T;
type Result = Awaited<Promise<Promise<string>>>; // string
// Extract function first parameter
type FirstParam<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type First = FirstParam<(name: string, age: number) => void>; // stringPractical Conditional Types
// API response helper
type ApiResult<T> =
| { success: true; data: T }
| { success: false; error: string };
// Extract data type from result
type ExtractData<T> = T extends { success: true; data: infer D } ? D : never;
type UserResult = ApiResult<User>;
type UserData = ExtractData<UserResult>; // User
// Type-safe event handlers
type EventHandler<T> = T extends `on${infer Event}`
? (event: Event) => void
: never;
type ClickHandler = EventHandler<"onClick">; // (event: "Click") => void---
Template Literal Types
Basic Template Literals
type Greeting = `Hello, ${string}!`;
const valid: Greeting = "Hello, World!"; // OK
const invalid: Greeting = "Hi, World!"; // Error
// Combine with unions
type Size = "small" | "medium" | "large";
type Color = "red" | "blue" | "green";
type ColoredSize = `${Color}-${Size}`;
// "red-small" | "red-medium" | "red-large" |
// "blue-small" | "blue-medium" | "blue-large" |
// "green-small" | "green-medium" | "green-large"String Manipulation Types
// Built-in string manipulation types
type Upper = Uppercase<"hello">; // "HELLO"
type Lower = Lowercase<"HELLO">; // "hello"
type Cap = Capitalize<"hello">; // "Hello"
type Uncap = Uncapitalize<"Hello">; // "hello"
// Practical: Generate event names
type Event = "click" | "hover" | "focus";
type EventHandler = `on${Capitalize<Event>}`;
// "onClick" | "onHover" | "onFocus"
// CSS property with vendor prefixes
type CSSProp = "transform" | "transition";
type Prefixed = `-webkit-${CSSProp}` | `-moz-${CSSProp}` | CSSProp;Advanced Template Patterns
// Parse dot-notation paths
type PathSegment<T> = T extends `${infer Head}.${infer Tail}`
? Head | PathSegment<Tail>
: T;
type Segments = PathSegment<"user.profile.name">;
// "user" | "profile" | "name"
// HTTP methods with paths
type Method = "GET" | "POST" | "PUT" | "DELETE";
type Endpoint = "/users" | "/posts" | "/comments";
type Route = `${Method} ${Endpoint}`;
// "GET /users" | "GET /posts" | "GET /comments" |
// "POST /users" | ... etc
// Type-safe SQL column references
type Table = "users" | "posts";
type Column<T extends Table> = T extends "users"
? "id" | "name" | "email"
: T extends "posts"
? "id" | "title" | "content"
: never;
type UserColumn = `users.${Column<"users">}`;
// "users.id" | "users.name" | "users.email"---
Variadic Tuple Types
Basic Variadic Tuples
// Spread tuple types
type Concat<T extends unknown[], U extends unknown[]> = [...T, ...U];
type Combined = Concat<[1, 2], [3, 4]>;
// [1, 2, 3, 4]
// Prepend element
type Prepend<T, U extends unknown[]> = [T, ...U];
type WithFirst = Prepend<0, [1, 2, 3]>;
// [0, 1, 2, 3]
// Append element
type Append<T extends unknown[], U> = [...T, U];
type WithLast = Append<[1, 2, 3], 4>;
// [1, 2, 3, 4]Practical Variadic Patterns
// Typed curry function
type Curry<F> = F extends (...args: infer A) => infer R
? A extends [infer First, ...infer Rest]
? (arg: First) => Curry<(...args: Rest) => R>
: R
: never;
declare function curry<F extends (...args: any[]) => any>(fn: F): Curry<F>;
function add(a: number, b: number, c: number): number {
return a + b + c;
}
const curriedAdd = curry(add);
const add1 = curriedAdd(1); // (arg: number) => Curry<...>
const add1and2 = add1(2); // (arg: number) => number
const result = add1and2(3); // number (6)
// Typed pipe function
type Pipe<T extends unknown[], R> = T extends [infer First, ...infer Rest]
? First extends (arg: R) => infer Next
? Pipe<Rest, Next>
: never
: R;
function pipe<T extends ((arg: any) => any)[]>(
...fns: T
): (arg: Parameters<T[0]>[0]) => Pipe<T, Parameters<T[0]>[0]> {
return (arg) => fns.reduce((acc, fn) => fn(acc), arg);
}
const process = pipe(
(n: number) => n * 2,
(n: number) => n.toString(),
(s: string) => s.length
);
const length = process(5); // number (2 - length of "10")---
Built-in Utility Types Reference
| Utility | Purpose | Example |
|---|---|---|
Partial<T> | All properties optional | Partial<User> |
Required<T> | All properties required | Required<Partial<User>> |
Readonly<T> | All properties readonly | Readonly<User> |
Pick<T, K> | Select properties | `Pick<User, "id" \ |
Omit<T, K> | Exclude properties | Omit<User, "password"> |
Record<K, V> | Create object type | Record<string, User> |
Exclude<T, U> | Remove union members | `Exclude<"a" \ |
Extract<T, U> | Keep union members | `Extract<"a" \ |
NonNullable<T> | Remove null/undefined | `NonNullable<string \ |
Parameters<F> | Function parameters | Parameters<typeof fn> |
ReturnType<F> | Function return | ReturnType<typeof fn> |
ConstructorParameters<C> | Constructor params | ConstructorParameters<typeof Date> |
InstanceType<C> | Instance type | InstanceType<typeof Date> |
Awaited<T> | Unwrap Promise | Awaited<Promise<User>> |
NoInfer<T> | Prevent inference | NoInfer<T> (TS 5.4+) |
NestJS Integration Reference
Load when: User asks about NestJS with TypeScript, API development, DTOs, validation, authentication, or backend patterns.
Type-safe API development with NestJS 11+.
Contents
- Project Structure
- Controllers and Routes
- DTOs and Validation
- Services and Dependency Injection
- Authentication
- Error Handling
---
Project Structure
Recommended Layout
src/
├── main.ts # Application entry point
├── app.module.ts # Root module
├── common/ # Shared utilities
│ ├── decorators/
│ ├── filters/
│ ├── guards/
│ ├── interceptors/
│ └── pipes/
├── config/ # Configuration
│ ├── config.module.ts
│ └── env.validation.ts
└── modules/
├── users/
│ ├── users.module.ts
│ ├── users.controller.ts
│ ├── users.service.ts
│ ├── users.repository.ts
│ ├── dto/
│ │ ├── create-user.dto.ts
│ │ └── update-user.dto.ts
│ ├── entities/
│ │ └── user.entity.ts
│ └── __tests__/
└── auth/
└── ...Module Configuration
// users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { UsersRepository } from './users.repository';
@Module({
controllers: [UsersController],
providers: [UsersService, UsersRepository],
exports: [UsersService] // Export for use in other modules
})
export class UsersModule {}---
Controllers and Routes
Basic Controller
import {
Controller,
Get,
Post,
Put,
Delete,
Param,
Body,
Query,
HttpCode,
HttpStatus
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { UsersService } from './users.service';
import { CreateUserDto, UpdateUserDto, UserResponseDto } from './dto';
import { PaginationDto } from '@/common/dto';
@ApiTags('users')
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
@ApiOperation({ summary: 'Get all users' })
@ApiResponse({ status: 200, type: [UserResponseDto] })
async findAll(@Query() query: PaginationDto): Promise<UserResponseDto[]> {
return this.usersService.findAll(query);
}
@Get(':id')
@ApiOperation({ summary: 'Get user by ID' })
@ApiResponse({ status: 200, type: UserResponseDto })
@ApiResponse({ status: 404, description: 'User not found' })
async findOne(@Param('id') id: string): Promise<UserResponseDto> {
return this.usersService.findOne(id);
}
@Post()
@HttpCode(HttpStatus.CREATED)
@ApiOperation({ summary: 'Create new user' })
@ApiResponse({ status: 201, type: UserResponseDto })
async create(@Body() dto: CreateUserDto): Promise<UserResponseDto> {
return this.usersService.create(dto);
}
@Put(':id')
@ApiOperation({ summary: 'Update user' })
async update(
@Param('id') id: string,
@Body() dto: UpdateUserDto
): Promise<UserResponseDto> {
return this.usersService.update(id, dto);
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Delete user' })
async remove(@Param('id') id: string): Promise<void> {
return this.usersService.remove(id);
}
}---
DTOs and Validation
Class-Validator Approach
// dto/create-user.dto.ts
import {
IsString,
IsEmail,
MinLength,
MaxLength,
IsOptional,
IsEnum,
ValidateNested,
IsArray
} from 'class-validator';
import { Type } from 'class-transformer';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export enum UserRole {
User = 'user',
Admin = 'admin',
Moderator = 'moderator'
}
export class AddressDto {
@ApiProperty()
@IsString()
street: string;
@ApiProperty()
@IsString()
city: string;
@ApiProperty()
@IsString()
country: string;
}
export class CreateUserDto {
@ApiProperty({ example: 'john@example.com' })
@IsEmail()
email: string;
@ApiProperty({ minLength: 2, maxLength: 50 })
@IsString()
@MinLength(2)
@MaxLength(50)
name: string;
@ApiProperty({ minLength: 8 })
@IsString()
@MinLength(8)
password: string;
@ApiPropertyOptional({ enum: UserRole, default: UserRole.User })
@IsOptional()
@IsEnum(UserRole)
role?: UserRole = UserRole.User;
@ApiPropertyOptional({ type: AddressDto })
@IsOptional()
@ValidateNested()
@Type(() => AddressDto)
address?: AddressDto;
@ApiPropertyOptional({ type: [String] })
@IsOptional()
@IsArray()
@IsString({ each: true })
tags?: string[];
}
// dto/update-user.dto.ts
import { PartialType } from '@nestjs/swagger';
import { CreateUserDto } from './create-user.dto';
export class UpdateUserDto extends PartialType(CreateUserDto) {}Zod-Based DTOs (Modern Approach)
// dto/user.schema.ts
import { z } from 'zod';
import { createZodDto } from 'nestjs-zod';
// Define Zod schemas
export const UserRoleSchema = z.enum(['user', 'admin', 'moderator']);
export const AddressSchema = z.object({
street: z.string(),
city: z.string(),
country: z.string()
});
export const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(2).max(50),
password: z.string().min(8),
role: UserRoleSchema.default('user').optional(),
address: AddressSchema.optional(),
tags: z.array(z.string()).optional()
});
export const UpdateUserSchema = CreateUserSchema.partial();
// Create DTO classes from schemas
export class CreateUserDto extends createZodDto(CreateUserSchema) {}
export class UpdateUserDto extends createZodDto(UpdateUserSchema) {}
// Infer types
export type CreateUser = z.infer<typeof CreateUserSchema>;
export type UpdateUser = z.infer<typeof UpdateUserSchema>;Global Validation Pipe
// main.ts
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // Strip unknown properties
forbidNonWhitelisted: true, // Throw on unknown properties
transform: true, // Transform payloads to DTO classes
transformOptions: {
enableImplicitConversion: true
}
})
);
await app.listen(3000);
}
bootstrap();---
Services and Dependency Injection
Typed Service
// users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { UsersRepository } from './users.repository';
import { CreateUserDto, UpdateUserDto, UserResponseDto } from './dto';
import { PaginationDto } from '@/common/dto';
@Injectable()
export class UsersService {
constructor(private readonly usersRepository: UsersRepository) {}
async findAll(query: PaginationDto): Promise<UserResponseDto[]> {
const users = await this.usersRepository.findAll(query);
return users.map(this.toResponseDto);
}
async findOne(id: string): Promise<UserResponseDto> {
const user = await this.usersRepository.findById(id);
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`);
}
return this.toResponseDto(user);
}
async create(dto: CreateUserDto): Promise<UserResponseDto> {
const user = await this.usersRepository.create(dto);
return this.toResponseDto(user);
}
async update(id: string, dto: UpdateUserDto): Promise<UserResponseDto> {
const user = await this.usersRepository.update(id, dto);
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`);
}
return this.toResponseDto(user);
}
async remove(id: string): Promise<void> {
const deleted = await this.usersRepository.delete(id);
if (!deleted) {
throw new NotFoundException(`User with ID ${id} not found`);
}
}
private toResponseDto(user: User): UserResponseDto {
return {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
createdAt: user.createdAt
};
}
}Repository Pattern
// users.repository.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '@/prisma/prisma.service';
import { User, Prisma } from '@prisma/client';
import { PaginationDto } from '@/common/dto';
@Injectable()
export class UsersRepository {
constructor(private readonly prisma: PrismaService) {}
async findAll(query: PaginationDto): Promise<User[]> {
return this.prisma.user.findMany({
skip: query.skip,
take: query.take,
orderBy: { createdAt: 'desc' }
});
}
async findById(id: string): Promise<User | null> {
return this.prisma.user.findUnique({ where: { id } });
}
async findByEmail(email: string): Promise<User | null> {
return this.prisma.user.findUnique({ where: { email } });
}
async create(data: Prisma.UserCreateInput): Promise<User> {
return this.prisma.user.create({ data });
}
async update(id: string, data: Prisma.UserUpdateInput): Promise<User | null> {
try {
return await this.prisma.user.update({ where: { id }, data });
} catch {
return null;
}
}
async delete(id: string): Promise<boolean> {
try {
await this.prisma.user.delete({ where: { id } });
return true;
} catch {
return false;
}
}
}---
Authentication
JWT Authentication
// auth/strategies/jwt.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
import { UsersService } from '@/modules/users/users.service';
interface JwtPayload {
sub: string;
email: string;
role: string;
iat: number;
exp: number;
}
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
configService: ConfigService,
private readonly usersService: UsersService
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.getOrThrow<string>('JWT_SECRET')
});
}
async validate(payload: JwtPayload) {
const user = await this.usersService.findOne(payload.sub);
if (!user) {
throw new UnauthorizedException();
}
return { id: payload.sub, email: payload.email, role: payload.role };
}
}Role-Based Access Control
// common/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
// common/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from '../decorators/roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<string[]>(
ROLES_KEY,
[context.getHandler(), context.getClass()]
);
if (!requiredRoles) {
return true;
}
const { user } = context.switchToHttp().getRequest();
return requiredRoles.includes(user.role);
}
}
// Usage in controller
@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
export class AdminController {
@Get('users')
@Roles('admin')
findAllUsers() {
return this.adminService.findAllUsers();
}
@Delete('users/:id')
@Roles('admin', 'moderator')
removeUser(@Param('id') id: string) {
return this.adminService.removeUser(id);
}
}---
Error Handling
Exception Filters
// common/filters/http-exception.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus
} from '@nestjs/common';
import { Response } from 'express';
interface ErrorResponse {
statusCode: number;
message: string;
error: string;
timestamp: string;
path: string;
}
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest();
let status = HttpStatus.INTERNAL_SERVER_ERROR;
let message = 'Internal server error';
let error = 'Internal Server Error';
if (exception instanceof HttpException) {
status = exception.getStatus();
const exceptionResponse = exception.getResponse();
if (typeof exceptionResponse === 'string') {
message = exceptionResponse;
} else if (typeof exceptionResponse === 'object') {
const responseObj = exceptionResponse as Record<string, unknown>;
message = (responseObj.message as string) || message;
error = (responseObj.error as string) || exception.name;
}
}
const errorResponse: ErrorResponse = {
statusCode: status,
message,
error,
timestamp: new Date().toISOString(),
path: request.url
};
response.status(status).json(errorResponse);
}
}
// main.ts
app.useGlobalFilters(new AllExceptionsFilter());Custom Exceptions
// common/exceptions/business.exception.ts
import { HttpException, HttpStatus } from '@nestjs/common';
export class BusinessException extends HttpException {
constructor(
message: string,
public readonly code: string,
status: HttpStatus = HttpStatus.BAD_REQUEST
) {
super({ message, code }, status);
}
}
export class InsufficientFundsException extends BusinessException {
constructor(required: number, available: number) {
super(
`Insufficient funds: required ${required}, available ${available}`,
'INSUFFICIENT_FUNDS'
);
}
}
export class DuplicateEmailException extends BusinessException {
constructor(email: string) {
super(`Email ${email} is already registered`, 'DUPLICATE_EMAIL');
}
}
// Usage
throw new InsufficientFundsException(100, 50);React Integration Reference
Load when: User asks about React with TypeScript, typed components, hooks, state management, or React patterns.
Type-safe React development patterns for React 19+.
Contents
---
Component Patterns
Functional Components
// Basic typed component
interface GreetingProps {
name: string;
age?: number;
}
function Greeting({ name, age }: GreetingProps) {
return (
<div>
Hello, {name}!
{age && <span> You are {age} years old.</span>}
</div>
);
}
// With React.FC (optional, some prefer explicit return type)
const GreetingFC: React.FC<GreetingProps> = ({ name, age }) => {
return <div>Hello, {name}!</div>;
};
// Component with children
interface CardProps {
title: string;
children: React.ReactNode;
}
function Card({ title, children }: CardProps) {
return (
<div className="card">
<h2>{title}</h2>
<div className="card-body">{children}</div>
</div>
);
}Generic Components
// Generic list component
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map((item, index) => (
<li key={keyExtractor(item)}>{renderItem(item, index)}</li>
))}
</ul>
);
}
// Usage
interface User {
id: string;
name: string;
}
<List<User>
items={users}
keyExtractor={(user) => user.id}
renderItem={(user) => <span>{user.name}</span>}
/>;
// Generic select component
interface SelectProps<T> {
options: T[];
value: T | null;
onChange: (value: T) => void;
getLabel: (option: T) => string;
getValue: (option: T) => string;
}
function Select<T>({
options,
value,
onChange,
getLabel,
getValue
}: SelectProps<T>) {
return (
<select
value={value ? getValue(value) : ""}
onChange={(e) => {
const selected = options.find((opt) => getValue(opt) === e.target.value);
if (selected) onChange(selected);
}}
>
<option value="">Select...</option>
{options.map((opt) => (
<option key={getValue(opt)} value={getValue(opt)}>
{getLabel(opt)}
</option>
))}
</select>
);
}Polymorphic Components
// Component that can render as different elements
type ButtonProps<T extends React.ElementType> = {
as?: T;
children: React.ReactNode;
variant?: "primary" | "secondary";
} & Omit<React.ComponentPropsWithoutRef<T>, "as" | "children">;
function Button<T extends React.ElementType = "button">({
as,
children,
variant = "primary",
...props
}: ButtonProps<T>) {
const Component = as || "button";
return (
<Component className={`btn btn-${variant}`} {...props}>
{children}
</Component>
);
}
// Usage
<Button>Click me</Button>
<Button as="a" href="/about">Link Button</Button>
<Button as={Link} to="/home">Router Link</Button>---
Hooks with TypeScript
useState
// Basic usage (type inferred)
const [count, setCount] = useState(0);
// Explicit type (for complex types or initial null)
const [user, setUser] = useState<User | null>(null);
// With union types
type Status = "idle" | "loading" | "success" | "error";
const [status, setStatus] = useState<Status>("idle");
// Lazy initialization
const [state, setState] = useState<ExpensiveState>(() => {
return computeExpensiveInitialState();
});useRef
// DOM element ref
const inputRef = useRef<HTMLInputElement>(null);
function focusInput() {
inputRef.current?.focus();
}
// Mutable ref (no initial render)
const intervalRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
intervalRef.current = setInterval(() => {}, 1000);
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, []);
// Ref to store previous value
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T | undefined>(undefined);
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}useReducer
// Define state and actions
interface CounterState {
count: number;
step: number;
}
type CounterAction =
| { type: "increment" }
| { type: "decrement" }
| { type: "setStep"; payload: number }
| { type: "reset" };
function counterReducer(
state: CounterState,
action: CounterAction
): CounterState {
switch (action.type) {
case "increment":
return { ...state, count: state.count + state.step };
case "decrement":
return { ...state, count: state.count - state.step };
case "setStep":
return { ...state, step: action.payload };
case "reset":
return { count: 0, step: 1 };
}
}
// Usage
const [state, dispatch] = useReducer(counterReducer, { count: 0, step: 1 });
dispatch({ type: "increment" });
dispatch({ type: "setStep", payload: 5 });Custom Hooks
// Async data fetching hook
interface UseAsyncResult<T> {
data: T | null;
loading: boolean;
error: Error | null;
refetch: () => void;
}
function useAsync<T>(
asyncFn: () => Promise<T>,
deps: React.DependencyList = []
): UseAsyncResult<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const execute = useCallback(async () => {
setLoading(true);
setError(null);
try {
const result = await asyncFn();
setData(result);
} catch (e) {
setError(e instanceof Error ? e : new Error(String(e)));
} finally {
setLoading(false);
}
}, deps);
useEffect(() => {
execute();
}, [execute]);
return { data, loading, error, refetch: execute };
}
// Usage
const { data: users, loading, error } = useAsync(
() => fetch("/api/users").then((r) => r.json()),
[]
);
// Local storage hook
function useLocalStorage<T>(
key: string,
initialValue: T
): [T, (value: T | ((prev: T) => T)) => void] {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = (value: T | ((prev: T) => T)) => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
localStorage.setItem(key, JSON.stringify(valueToStore));
};
return [storedValue, setValue];
}---
State Management
Zustand with TypeScript
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
// Define state interface
interface AuthState {
user: User | null;
token: string | null;
isAuthenticated: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
setUser: (user: User) => void;
}
// Create typed store
const useAuthStore = create<AuthState>()(
devtools(
persist(
(set) => ({
user: null,
token: null,
isAuthenticated: false,
login: async (email, password) => {
const response = await api.login(email, password);
set({
user: response.user,
token: response.token,
isAuthenticated: true
});
},
logout: () => {
set({ user: null, token: null, isAuthenticated: false });
},
setUser: (user) => set({ user })
}),
{ name: 'auth-storage' }
)
)
);
// Usage with selectors
const user = useAuthStore((state) => state.user);
const login = useAuthStore((state) => state.login);
// Shallow comparison for multiple values
import { useShallow } from 'zustand/react/shallow';
const { user, isAuthenticated } = useAuthStore(
useShallow((state) => ({
user: state.user,
isAuthenticated: state.isAuthenticated
}))
);Redux Toolkit with TypeScript
import { createSlice, PayloadAction, configureStore } from '@reduxjs/toolkit';
// Define slice state
interface TodosState {
items: Todo[];
filter: "all" | "active" | "completed";
loading: boolean;
}
const initialState: TodosState = {
items: [],
filter: "all",
loading: false
};
// Create typed slice
const todosSlice = createSlice({
name: "todos",
initialState,
reducers: {
addTodo: (state, action: PayloadAction<string>) => {
state.items.push({
id: crypto.randomUUID(),
text: action.payload,
completed: false
});
},
toggleTodo: (state, action: PayloadAction<string>) => {
const todo = state.items.find((t) => t.id === action.payload);
if (todo) {
todo.completed = !todo.completed;
}
},
setFilter: (state, action: PayloadAction<TodosState["filter"]>) => {
state.filter = action.payload;
}
}
});
// Configure store with type inference
const store = configureStore({
reducer: {
todos: todosSlice.reducer
}
});
// Infer types from store
type RootState = ReturnType<typeof store.getState>;
type AppDispatch = typeof store.dispatch;
// Typed hooks
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
const useAppDispatch = () => useDispatch<AppDispatch>();
const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
// Usage
const todos = useAppSelector((state) => state.todos.items);
const dispatch = useAppDispatch();
dispatch(todosSlice.actions.addTodo("New todo"));---
Event Handling
Common Event Types
// Click events
function handleClick(event: React.MouseEvent<HTMLButtonElement>) {
console.log(event.currentTarget.name);
}
// Form events
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const formData = new FormData(event.currentTarget);
}
// Input change
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
const { name, value, checked, type } = event.target;
const inputValue = type === "checkbox" ? checked : value;
}
// Keyboard events
function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
if (event.key === "Enter") {
event.preventDefault();
// submit form
}
}
// Focus events
function handleFocus(event: React.FocusEvent<HTMLInputElement>) {
event.target.select();
}
// Drag events
function handleDrag(event: React.DragEvent<HTMLDivElement>) {
event.dataTransfer.setData("text/plain", "dragged data");
}Form with TypeScript
interface FormData {
name: string;
email: string;
role: "user" | "admin";
}
function RegistrationForm() {
const [formData, setFormData] = useState<FormData>({
name: "",
email: "",
role: "user"
});
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log(formData);
};
return (
<form onSubmit={handleSubmit}>
<input
name="name"
value={formData.name}
onChange={handleChange}
/>
<input
name="email"
type="email"
value={formData.email}
onChange={handleChange}
/>
<select name="role" value={formData.role} onChange={handleChange}>
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
<button type="submit">Register</button>
</form>
);
}---
Context API
Typed Context
// Define context type
interface ThemeContextType {
theme: "light" | "dark";
toggleTheme: () => void;
}
// Create context with undefined default
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
// Provider component
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<"light" | "dark">("light");
const toggleTheme = useCallback(() => {
setTheme((prev) => (prev === "light" ? "dark" : "light"));
}, []);
const value = useMemo(() => ({ theme, toggleTheme }), [theme, toggleTheme]);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
// Custom hook with type safety
function useTheme(): ThemeContextType {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error("useTheme must be used within ThemeProvider");
}
return context;
}
// Usage
function ThemeToggle() {
const { theme, toggleTheme } = useTheme();
return <button onClick={toggleTheme}>Current: {theme}</button>;
}Generic Context Factory
// Factory function for creating typed contexts
function createContext<T>(displayName: string) {
const Context = React.createContext<T | undefined>(undefined);
Context.displayName = displayName;
function useContextHook(): T {
const context = React.useContext(Context);
if (context === undefined) {
throw new Error(`use${displayName} must be used within ${displayName}Provider`);
}
return context;
}
return [Context.Provider, useContextHook] as const;
}
// Usage
interface AuthContextType {
user: User | null;
login: (credentials: Credentials) => Promise<void>;
logout: () => void;
}
const [AuthProvider, useAuth] = createContext<AuthContextType>("Auth");Modern Toolchain Reference
Load when: User asks about Vite, pnpm, ESLint, Vitest, tsconfig, build tools, or project configuration.
Modern TypeScript toolchain configuration for 2025.
Contents
- TypeScript Configuration
- Package Manager (pnpm)
- Build Tool (Vite)
- Linting (ESLint 9)
- Testing (Vitest)
- Formatting (Prettier)
---
TypeScript Configuration
Strict Enterprise Configuration
// tsconfig.json
{
"compilerOptions": {
// Language and Environment
"target": "ES2024",
"lib": ["ES2024"],
"module": "ESNext",
"moduleResolution": "bundler",
// Strict Type Checking
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
// Module Handling
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
// Output
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
// Path Aliases
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
// Performance
"skipLibCheck": true,
"incremental": true,
"tsBuildInfoFile": "./node_modules/.cache/tsbuildinfo"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}React Project Configuration
// tsconfig.json for React
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"esModuleInterop": true,
"isolatedModules": true,
"skipLibCheck": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@components/*": ["./src/components/*"],
"@hooks/*": ["./src/hooks/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}Node.js Backend Configuration
// tsconfig.json for Node.js/NestJS
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"esModuleInterop": true,
"isolatedModules": true,
"skipLibCheck": true,
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.spec.ts"]
}---
Package Manager (pnpm)
Why pnpm
| Feature | npm | pnpm |
|---|---|---|
| Disk usage | Duplicates packages | Shared store, symlinks |
| Install speed | Slower | 2-3x faster |
| Strictness | Allows phantom deps | Strict by default |
| Monorepo support | Basic workspaces | First-class support |
Basic Commands
# Install dependencies
pnpm install
# Add packages
pnpm add typescript
pnpm add -D vitest @types/node
# Run scripts
pnpm run build
pnpm test
# Update packages
pnpm update
pnpm update --interactive
# List packages
pnpm list
pnpm why lodash
# Clean install
pnpm install --frozen-lockfileWorkspace Configuration
# pnpm-workspace.yaml
packages:
- 'packages/*'
- 'apps/*'// package.json (root)
{
"name": "my-monorepo",
"private": true,
"scripts": {
"build": "pnpm -r run build",
"test": "pnpm -r run test",
"lint": "pnpm -r run lint"
}
}---
Build Tool (Vite)
Vite Configuration
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
plugins: [
react(),
tsconfigPaths()
],
server: {
port: 3000,
host: true
},
build: {
target: 'es2022',
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
utils: ['lodash-es', 'date-fns']
}
}
}
},
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts']
}
});Library Mode
// vite.config.ts for library
import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
export default defineConfig({
build: {
lib: {
entry: './src/index.ts',
name: 'MyLibrary',
fileName: 'my-library',
formats: ['es', 'cjs']
},
rollupOptions: {
external: ['react', 'react-dom'],
output: {
globals: {
react: 'React',
'react-dom': 'ReactDOM'
}
}
}
},
plugins: [
dts({ insertTypesEntry: true })
]
});---
Linting (ESLint 9)
Flat Config Format
// eslint.config.js
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import reactPlugin from 'eslint-plugin-react';
import reactHooksPlugin from 'eslint-plugin-react-hooks';
export default tseslint.config(
// Base ESLint recommendations
eslint.configs.recommended,
// TypeScript strict type-checking
...tseslint.configs.strictTypeChecked,
...tseslint.configs.stylisticTypeChecked,
// TypeScript parser options
{
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname
}
}
},
// React configuration
{
files: ['**/*.tsx'],
plugins: {
react: reactPlugin,
'react-hooks': reactHooksPlugin
},
rules: {
...reactPlugin.configs.recommended.rules,
...reactHooksPlugin.configs.recommended.rules,
'react/react-in-jsx-scope': 'off'
},
settings: {
react: { version: 'detect' }
}
},
// Custom rules
{
rules: {
'@typescript-eslint/no-unused-vars': ['error', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_'
}],
'@typescript-eslint/consistent-type-imports': ['error', {
prefer: 'type-imports'
}],
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/await-thenable': 'error'
}
},
// Ignore patterns
{
ignores: ['dist/**', 'node_modules/**', '*.config.js']
}
);Common Rules Explained
// Important TypeScript ESLint rules
{
rules: {
// Enforce type imports for better tree-shaking
'@typescript-eslint/consistent-type-imports': 'error',
// Prevent unhandled promises
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-misused-promises': 'error',
// Prevent awaiting non-promises
'@typescript-eslint/await-thenable': 'error',
// Require return types on functions
'@typescript-eslint/explicit-function-return-type': ['error', {
allowExpressions: true
}],
// Prefer nullish coalescing
'@typescript-eslint/prefer-nullish-coalescing': 'error',
// Prefer optional chaining
'@typescript-eslint/prefer-optional-chain': 'error',
// No any type
'@typescript-eslint/no-explicit-any': 'error',
// Enforce strict boolean expressions
'@typescript-eslint/strict-boolean-expressions': 'error'
}
}---
Testing (Vitest)
Vitest Configuration
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
plugins: [react(), tsconfigPaths()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
include: ['src/**/*.{test,spec}.{ts,tsx}'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: ['**/*.d.ts', '**/*.config.*', '**/test/**']
},
typecheck: {
enabled: true
}
}
});Test Setup
// src/test/setup.ts
import '@testing-library/jest-dom/vitest';
import { cleanup } from '@testing-library/react';
import { afterEach, beforeAll, afterAll, vi } from 'vitest';
// Cleanup after each test
afterEach(() => {
cleanup();
});
// Mock environment variables
beforeAll(() => {
vi.stubEnv('API_URL', 'http://localhost:3000');
});
afterAll(() => {
vi.unstubAllEnvs();
});Example Tests
// src/utils/format.test.ts
import { describe, it, expect } from 'vitest';
import { formatCurrency, formatDate } from './format';
describe('formatCurrency', () => {
it('formats USD correctly', () => {
expect(formatCurrency(1234.56, 'USD')).toBe('$1,234.56');
});
it('handles zero', () => {
expect(formatCurrency(0, 'USD')).toBe('$0.00');
});
});
// src/components/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { Button } from './Button';
describe('Button', () => {
it('renders with label', () => {
render(<Button label="Click me" onClick={() => {}} />);
expect(screen.getByRole('button')).toHaveTextContent('Click me');
});
it('calls onClick when clicked', () => {
const handleClick = vi.fn();
render(<Button label="Click" onClick={handleClick} />);
fireEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledOnce();
});
});---
Formatting (Prettier)
Prettier Configuration
// .prettierrc
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 100,
"bracketSpacing": true,
"arrowParens": "always",
"endOfLine": "lf"
}Integration with ESLint
// eslint.config.js
import eslintConfigPrettier from 'eslint-config-prettier';
export default tseslint.config(
// ... other configs
eslintConfigPrettier // Must be last to disable conflicting rules
);Package Scripts
// package.json
{
"scripts": {
"format": "prettier --write .",
"format:check": "prettier --check .",
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"typecheck": "tsc --noEmit"
}
}---
Complete Project Setup
Quick Start Script
#!/bin/bash
# setup-ts-project.sh
PROJECT_NAME=${1:-my-app}
# Create project with Vite
pnpm create vite@latest $PROJECT_NAME --template react-ts
cd $PROJECT_NAME
# Install dependencies
pnpm install
# Add development dependencies
pnpm add -D \
typescript-eslint \
@eslint/js \
eslint-plugin-react \
eslint-plugin-react-hooks \
eslint-config-prettier \
prettier \
vitest \
@vitest/coverage-v8 \
@testing-library/react \
@testing-library/jest-dom \
vite-tsconfig-paths
echo "Project setup complete! Run: cd $PROJECT_NAME && pnpm dev"TypeScript Type System Reference
Load when: User asks about type annotations, interfaces vs types, unions, intersections, or type system fundamentals.
Complete guide to TypeScript's structural type system.
Contents
- Type Annotations
- Interfaces vs Type Aliases
- Union and Intersection Types
- Literal Types
- Type Guards and Narrowing
- The satisfies Operator
---
Type Annotations
Variable Annotations
// Explicit type annotations
const name: string = "Alice";
const age: number = 30;
const active: boolean = true;
// Type inference (preferred when obvious)
const inferredName = "Bob"; // TypeScript infers string
const inferredAge = 25; // TypeScript infers number
// Arrays
const numbers: number[] = [1, 2, 3];
const strings: Array<string> = ["a", "b", "c"];
// Tuples (fixed-length arrays with specific types)
const pair: [string, number] = ["age", 30];
const triple: [string, number, boolean] = ["name", 1, true];Function Annotations
// Function with typed parameters and return
function greet(name: string): string {
return `Hello, ${name}!`;
}
// Arrow function
const add = (a: number, b: number): number => a + b;
// Optional parameters
function greetOptional(name: string, greeting?: string): string {
return `${greeting ?? "Hello"}, ${name}!`;
}
// Default parameters
function greetDefault(name: string, greeting: string = "Hello"): string {
return `${greeting}, ${name}!`;
}
// Rest parameters
function sum(...numbers: number[]): number {
return numbers.reduce((a, b) => a + b, 0);
}
// Function type alias
type Comparator<T> = (a: T, b: T) => number;
const numberCompare: Comparator<number> = (a, b) => a - b;---
Interfaces vs Type Aliases
When to Use Interfaces
// Interfaces are ideal for object shapes
interface User {
id: string;
name: string;
email: string;
}
// Interfaces can be extended
interface Employee extends User {
employeeId: string;
department: string;
}
// Interfaces can be implemented by classes
class Manager implements Employee {
constructor(
public id: string,
public name: string,
public email: string,
public employeeId: string,
public department: string
) {}
}
// Declaration merging (interfaces only)
interface Config {
apiUrl: string;
}
interface Config {
timeout: number;
}
// Config now has both apiUrl and timeoutWhen to Use Type Aliases
// Type aliases for unions
type Status = "pending" | "approved" | "rejected";
// Type aliases for complex types
type Handler = (event: Event) => void;
// Type aliases for mapped types
type Nullable<T> = { [K in keyof T]: T[K] | null };
// Type aliases for conditional types
type NonNullable<T> = T extends null | undefined ? never : T;
// Type aliases for tuples
type Point = [x: number, y: number];
type RGB = [red: number, green: number, blue: number];Decision Guide
| Use Case | Prefer |
|---|---|
| Object shapes | interface |
| Extending objects | interface |
| Class contracts | interface |
| Union types | type |
| Tuple types | type |
| Mapped types | type |
| Conditional types | type |
| Primitive aliases | type |
---
Union and Intersection Types
Union Types
// Value can be one of several types
type StringOrNumber = string | number;
// Discriminated unions (tagged unions)
interface Dog {
kind: "dog";
bark(): void;
}
interface Cat {
kind: "cat";
meow(): void;
}
type Pet = Dog | Cat;
function speak(pet: Pet): void {
switch (pet.kind) {
case "dog":
pet.bark();
break;
case "cat":
pet.meow();
break;
}
}Intersection Types
// Value must satisfy all types
interface HasName {
name: string;
}
interface HasAge {
age: number;
}
type Person = HasName & HasAge;
const person: Person = {
name: "Alice",
age: 30
};
// Practical: Extending with additional properties
type WithTimestamp<T> = T & { createdAt: Date; updatedAt: Date };
interface Article {
title: string;
content: string;
}
type TimestampedArticle = WithTimestamp<Article>;---
Literal Types
String Literals
// Specific string values
type Direction = "north" | "south" | "east" | "west";
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
function move(direction: Direction): void {
console.log(`Moving ${direction}`);
}
move("north"); // OK
move("up"); // Error: Argument of type '"up"' is not assignableNumeric Literals
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
type BinaryDigit = 0 | 1;
function roll(): DiceRoll {
return Math.ceil(Math.random() * 6) as DiceRoll;
}Template Literal Types
// Construct string literal types
type EventName = "click" | "hover" | "focus";
type HandlerName = `on${Capitalize<EventName>}`;
// "onClick" | "onHover" | "onFocus"
// CSS unit types
type CSSUnit = "px" | "em" | "rem" | "%";
type CSSValue = `${number}${CSSUnit}`;
const width: CSSValue = "100px"; // OK
const height: CSSValue = "50%"; // OK
const bad: CSSValue = "100"; // Error---
Type Guards and Narrowing
Built-in Type Guards
function process(value: string | number | null): string {
// typeof guard
if (typeof value === "string") {
return value.toUpperCase();
}
// typeof guard for number
if (typeof value === "number") {
return value.toFixed(2);
}
// null/undefined narrowing
if (value === null) {
return "null";
}
// Exhaustiveness check
const _exhaustive: never = value;
throw new Error(`Unhandled case: ${_exhaustive}`);
}instanceof Guard
class ApiError extends Error {
constructor(public statusCode: number, message: string) {
super(message);
}
}
class ValidationError extends Error {
constructor(public fields: string[]) {
super("Validation failed");
}
}
function handleError(error: Error): void {
if (error instanceof ApiError) {
console.log(`API Error ${error.statusCode}: ${error.message}`);
} else if (error instanceof ValidationError) {
console.log(`Validation Error on fields: ${error.fields.join(", ")}`);
} else {
console.log(`Unknown error: ${error.message}`);
}
}Custom Type Guards
interface User {
type: "user";
name: string;
}
interface Admin {
type: "admin";
name: string;
permissions: string[];
}
type Account = User | Admin;
// Type predicate: returns boolean but narrows type
function isAdmin(account: Account): account is Admin {
return account.type === "admin";
}
function getPermissions(account: Account): string[] {
if (isAdmin(account)) {
return account.permissions; // TypeScript knows this is Admin
}
return [];
}Assertion Functions
function assertDefined<T>(value: T | undefined, message: string): asserts value is T {
if (value === undefined) {
throw new Error(message);
}
}
function processUser(user: User | undefined): void {
assertDefined(user, "User is required");
// After assertion, user is narrowed to User
console.log(user.name);
}---
The satisfies Operator
Problem: Type Assertions Hide Bugs
// Using 'as' can hide type errors
const config = {
port: 3000,
host: "localhost"
} as Record<string, string | number>;
// No error, but port is now string | number
const portString = config.port.toFixed(2); // Runtime error if port is string!Solution: satisfies Validates Without Widening
// satisfies checks conformance but preserves literal types
const config = {
port: 3000,
host: "localhost"
} satisfies Record<string, string | number>;
// TypeScript knows port is number, host is string
config.port.toFixed(2); // OK - port is number
config.host.toUpperCase(); // OK - host is stringPractical Use Cases
// Color palette with constrained values
const palette = {
primary: "#007bff",
secondary: "#6c757d",
success: "#28a745"
} satisfies Record<string, `#${string}`>;
// TypeScript knows each property exists and is a hex string
palette.primary.startsWith("#"); // OK
// Route configuration
type RouteConfig = {
path: string;
method: "GET" | "POST";
handler: () => void;
};
const routes = {
home: { path: "/", method: "GET", handler: () => {} },
login: { path: "/login", method: "POST", handler: () => {} }
} satisfies Record<string, RouteConfig>;
// TypeScript preserves literal types for each route
routes.home.method; // "GET" (not "GET" | "POST")---
Special Types
any vs unknown
// any: Opt out of type checking (avoid)
let anyValue: any = "hello";
anyValue.toFixed(2); // No error, but crashes at runtime
// unknown: Type-safe any (prefer)
let unknownValue: unknown = "hello";
unknownValue.toFixed(2); // Error: Object is of type 'unknown'
// Must narrow unknown before use
if (typeof unknownValue === "string") {
unknownValue.toUpperCase(); // OK after narrowing
}never
// never: Represents impossible values
function fail(message: string): never {
throw new Error(message);
}
// Exhaustiveness checking with never
type Shape = "circle" | "square";
function getArea(shape: Shape): number {
switch (shape) {
case "circle":
return Math.PI;
case "square":
return 1;
default:
// If we add a new shape, this will error
const _exhaustive: never = shape;
throw new Error(`Unknown shape: ${_exhaustive}`);
}
}void vs undefined
// void: Function doesn't return anything meaningful
function log(message: string): void {
console.log(message);
}
// undefined: Explicit undefined value
function findUser(id: string): User | undefined {
return users.get(id);
}#!/usr/bin/env bash
#
# TypeScript Project Setup Validator
#
# Checks that TypeScript project is properly configured for development.
# Run before starting development or deploying.
#
# Usage: ./validate-setup.sh
#
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
ERRORS=0
WARNINGS=0
pass() { echo -e "${GREEN}✓${NC} $1"; }
fail() { echo -e "${RED}✗${NC} $1"; ((ERRORS++)); }
warn() { echo -e "${YELLOW}!${NC} $1"; ((WARNINGS++)); }
info() { echo -e "${BLUE}ℹ${NC} $1"; }
echo "=========================================="
echo "TypeScript Project Setup Validator"
echo "=========================================="
echo ""
# Check 1: Node.js version
echo "Checking Node.js..."
if command -v node &> /dev/null; then
NODE_VERSION=$(node -v | sed 's/v//')
MAJOR_VERSION=$(echo $NODE_VERSION | cut -d. -f1)
if [[ $MAJOR_VERSION -ge 20 ]]; then
pass "Node.js $NODE_VERSION (recommended: 20+)"
else
warn "Node.js $NODE_VERSION (recommended: 20+ for full ES2024 support)"
fi
else
fail "Node.js not found"
fi
# Check 2: Package manager
echo ""
echo "Checking package manager..."
if command -v pnpm &> /dev/null; then
PNPM_VERSION=$(pnpm -v)
pass "pnpm $PNPM_VERSION installed (recommended)"
elif command -v npm &> /dev/null; then
NPM_VERSION=$(npm -v)
warn "npm $NPM_VERSION installed (pnpm recommended for better performance)"
else
fail "No package manager found"
fi
# Check 3: TypeScript installation
echo ""
echo "Checking TypeScript..."
if [[ -f "node_modules/typescript/package.json" ]]; then
TS_VERSION=$(node -p "require('./node_modules/typescript/package.json').version")
MAJOR_VERSION=$(echo $TS_VERSION | cut -d. -f1)
MINOR_VERSION=$(echo $TS_VERSION | cut -d. -f2)
if [[ $MAJOR_VERSION -ge 5 ]] && [[ $MINOR_VERSION -ge 5 ]]; then
pass "TypeScript $TS_VERSION installed"
else
warn "TypeScript $TS_VERSION installed (5.5+ recommended)"
fi
elif command -v tsc &> /dev/null; then
TS_VERSION=$(tsc --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
pass "TypeScript $TS_VERSION (global)"
else
fail "TypeScript not found"
fi
# Check 4: tsconfig.json
echo ""
echo "Checking TypeScript configuration..."
if [[ -f "tsconfig.json" ]]; then
pass "tsconfig.json exists"
# Check for strict mode
if grep -q '"strict":\s*true' tsconfig.json 2>/dev/null; then
pass "Strict mode enabled"
else
warn "Strict mode not enabled (recommended)"
fi
# Check for target
if grep -qE '"target":\s*"ES202[234]"' tsconfig.json 2>/dev/null; then
pass "Modern ES target configured"
else
info "Consider updating target to ES2024"
fi
else
fail "tsconfig.json not found"
fi
# Check 5: package.json type field
echo ""
echo "Checking ESM configuration..."
if [[ -f "package.json" ]]; then
if grep -q '"type":\s*"module"' package.json 2>/dev/null; then
pass "ESM mode enabled (type: module)"
else
info "Consider adding \"type\": \"module\" for ESM"
fi
else
fail "package.json not found"
fi
# Check 6: ESLint configuration
echo ""
echo "Checking linting setup..."
if [[ -f "eslint.config.js" ]] || [[ -f "eslint.config.mjs" ]]; then
pass "ESLint flat config found"
elif [[ -f ".eslintrc.js" ]] || [[ -f ".eslintrc.json" ]]; then
warn "Legacy ESLint config found (migrate to flat config for ESLint 9+)"
else
warn "ESLint not configured"
fi
# Check 7: Prettier configuration
echo ""
echo "Checking formatting setup..."
if [[ -f ".prettierrc" ]] || [[ -f ".prettierrc.json" ]] || [[ -f "prettier.config.js" ]]; then
pass "Prettier configured"
else
info "Consider adding Prettier for consistent formatting"
fi
# Check 8: Test framework
echo ""
echo "Checking test setup..."
if [[ -f "vitest.config.ts" ]] || [[ -f "vitest.config.js" ]]; then
pass "Vitest configured"
elif [[ -f "jest.config.ts" ]] || [[ -f "jest.config.js" ]]; then
pass "Jest configured"
elif grep -q '"vitest"' package.json 2>/dev/null; then
pass "Vitest in dependencies"
elif grep -q '"jest"' package.json 2>/dev/null; then
pass "Jest in dependencies"
else
warn "No test framework detected"
fi
# Check 9: Git hooks (optional)
echo ""
echo "Checking Git hooks..."
if [[ -d ".husky" ]]; then
pass "Husky Git hooks configured"
elif [[ -f ".git/hooks/pre-commit" ]]; then
pass "Git pre-commit hook exists"
else
info "Consider adding pre-commit hooks for quality checks"
fi
# Check 10: Dependencies up to date
echo ""
echo "Checking for outdated packages..."
if command -v pnpm &> /dev/null && [[ -f "pnpm-lock.yaml" ]]; then
OUTDATED=$(pnpm outdated --format json 2>/dev/null | grep -c '"' || echo "0")
if [[ "$OUTDATED" == "0" ]] || [[ "$OUTDATED" == "" ]]; then
pass "All packages up to date"
else
info "Some packages may be outdated (run: pnpm outdated)"
fi
elif command -v npm &> /dev/null && [[ -f "package-lock.json" ]]; then
info "Run 'npm outdated' to check for updates"
fi
# Summary
echo ""
echo "=========================================="
echo "Summary"
echo "=========================================="
if [[ $ERRORS -eq 0 ]] && [[ $WARNINGS -eq 0 ]]; then
echo -e "${GREEN}All checks passed!${NC}"
echo "Your TypeScript project is properly configured."
exit 0
elif [[ $ERRORS -eq 0 ]]; then
echo -e "${YELLOW}Passed with $WARNINGS warning(s)${NC}"
echo "Project is functional but could be improved."
exit 0
else
echo -e "${RED}Failed with $ERRORS error(s) and $WARNINGS warning(s)${NC}"
echo "Fix errors before proceeding."
exit 1
fi
Related skills
How it compares
Use mastering-typescript when the goal is strict ESLint 9 flat-config typing rules; use a general TypeScript tutorial skill when lint configuration is not the primary need.
FAQ
Which ESLint presets does mastering-typescript enable?
mastering-typescript enables @eslint/js recommended config plus typescript-eslint strictTypeChecked and stylisticTypeChecked bundles inside an ESLint 9+ eslint.config.js flat configuration file.
How does mastering-typescript configure type-aware linting?
mastering-typescript sets languageOptions.parserOptions.projectService to true and tsconfigRootDir to import.meta.dirname so typescript-eslint resolves project types for strict flat-config linting.
Is Mastering Typescript safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.