
Typescript Core
- 324 installs
- 63 repo stars
- Updated July 18, 2026
- bobmatnyc/claude-mpm-skills
typescript-core is an agent skill that applies TypeScript types, generics, narrowing, modules, and strict compiler options correctly for developers authoring or refactoring TypeScript codebases.
About
typescript-core is a bobmatnyc claude-mpm-skills module for disciplined TypeScript engineering. The skill guides correct use of types, generics, control-flow narrowing, module boundaries, and strict compiler options when writing or refactoring TypeScript across frontend and backend projects. It helps developers eliminate unsafe casts, tighten `tsconfig` strictness, structure shared types, and apply narrowing patterns that keep inference honest as codebases grow. Developers reach for typescript-core when enabling stricter compiler flags, untangling generic utility types, modularizing a monolith TS repo, or reviewing type safety before a major release. The module complements framework-specific skills by focusing on language mechanics that underpin React, Node, and shared package code alike.
- Strict typing defaults
- Generics and utility types
- Type narrowing patterns
- Module resolution basics
- Refactor-safe interfaces
Typescript Core by the numbers
- 324 all-time installs (skills.sh)
- Ranked #134 of 782 Skill Development skills by installs in the Skillselion catalog
- Data as of Aug 1, 2026 (Skillselion catalog sync)
npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill typescript-coreAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 324 |
|---|---|
| repo stars | ★ 63 |
| Last updated | July 18, 2026 |
| Repository | bobmatnyc/claude-mpm-skills ↗ |
How do you apply strict TypeScript patterns correctly?
Apply TypeScript types, generics, narrowing, modules, and strict compiler options correctly when authoring or refactoring TS codebases.
Who is it for?
Developers authoring or refactoring TypeScript codebases who need correct generics, narrowing, module structure, and strict compiler configuration.
Skip if: Developers working exclusively in untyped JavaScript without a TypeScript migration plan, or teams needing framework-specific UI patterns instead of core language mechanics.
When should I use this skill?
The user asks to fix TypeScript types, enable strict compiler options, apply generics or narrowing, or refactor TS module boundaries.
What you get
Refactored TypeScript modules with correct generics, narrowing, strict compiler settings, and safer shared type definitions.
- strict tsconfig updates
- typed module refactors
- shared type definitions
Files
TypeScript Core Patterns
Modern TypeScript development patterns for type safety, runtime validation, and optimal configuration.
Quick Start
New Project: Use 2025 tsconfig → Enable strict + noUncheckedIndexedAccess → Choose Zod for validation
Existing Project: Enable strict: false initially → Fix any with unknown → Add noUncheckedIndexedAccess
API Development: Zod schemas at boundaries → z.infer<typeof Schema> for types → satisfies for routes
Library Development: Enable declaration: true → Use const type parameters → See advanced-patterns-2025.md
Quick Reference
tsconfig.json 2025 Baseline
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"verbatimModuleSyntax": true,
"isolatedModules": true,
"skipLibCheck": true,
"declaration": true,
"declarationMap": true
}
}Key Compiler Options
| Option | Purpose | When to Enable |
|---|---|---|
noUncheckedIndexedAccess | Forces null checks on array/object access | Always for safety |
exactOptionalPropertyTypes | Distinguishes undefined from missing | APIs with optional fields |
verbatimModuleSyntax | Enforces explicit type-only imports | ESM projects |
erasableSyntaxOnly | Node.js 22+ native TS support | Type stripping environments |
Local Baselines
See references/configuration.md for repo-specific tsconfig patterns (CommonJS CLI, NodeNext strict, Next.js bundler).
Core Type Patterns
Const Type Parameters
Preserve literal types through generic functions:
function createConfig<const T extends Record<string, unknown>>(config: T): T {
return config;
}
const config = createConfig({
apiUrl: "https://api.example.com",
timeout: 5000
});
// Type: { readonly apiUrl: "https://api.example.com"; readonly timeout: 5000 }Satisfies Operator
Validate against a type while preserving literal inference:
type Route = { path: string; children?: Routes };
type Routes = Record<string, Route>;
const routes = {
AUTH: { path: "/auth" },
HOME: { path: "/" }
} satisfies Routes;
routes.AUTH.path; // Type: "/auth" (literal preserved)
routes.NONEXISTENT; // ❌ Type errorTemplate Literal Types
Type-safe string manipulation and route extraction:
type ExtractParams<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractParams<Rest>
: T extends `${string}:${infer Param}`
? Param
: never;
type Params = ExtractParams<"/users/:id/posts/:postId">; // "id" | "postId"Discriminated Unions with Exhaustiveness
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
function handleResult<T>(result: Result<T>): T {
if (result.success) return result.data;
throw result.error;
}
// Exhaustiveness checking
type Action =
| { type: 'create'; payload: string }
| { type: 'delete'; id: number };
function handle(action: Action) {
switch (action.type) {
case 'create': return action.payload;
case 'delete': return action.id;
default: {
const _exhaustive: never = action;
throw new Error(`Unhandled: ${_exhaustive}`);
}
}
}Runtime Validation
TypeScript types disappear at runtime. Use validation libraries for external data (APIs, forms, config files).
Quick Comparison
| Library | Bundle Size | Speed | Best For |
|---|---|---|---|
| Zod | ~13.5kB | Baseline | Full-stack apps, tRPC integration |
| TypeBox | ~8kB | ~10x faster | OpenAPI, performance-critical |
| Valibot | ~1.4kB | ~2x faster | Edge functions, minimal bundles |
Basic Pattern (Zod)
import { z } from "zod";
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
role: z.enum(["admin", "user", "guest"]),
});
type User = z.infer<typeof UserSchema>;
// Validate external data
function parseUser(input: unknown): User {
return UserSchema.parse(input);
}→ See [runtime-validation.md](./references/runtime-validation.md) for complete Zod, TypeBox, and Valibot patterns
Decision Support
Quick Decision Guide
Need to choose between `type` vs `interface`?
- Public API / library types →
interface - Union types / mapped types →
type - Simple object shapes →
interface(default)
Need generics or union types?
- Output type depends on input type → Generics
- Fixed set of known types → Union types
- Building reusable data structures → Generics
Dealing with unknown data?
- External data (API, user input) →
unknown(type-safe) - Rapid prototyping / migration →
any(temporarily)
Need runtime validation?
- Full-stack TypeScript with tRPC → Zod
- OpenAPI / high performance → TypeBox
- Edge functions / minimal bundle → Valibot
→ See [decision-trees.md](./references/decision-trees.md) for comprehensive decision frameworks
Troubleshooting
Common Issues Quick Reference
Property does not exist on type → Define proper interface or use optional properties
Type is not assignable → Fix property types or use runtime validation (Zod)
Object is possibly 'undefined' → Use optional chaining (?.) or type guards
Cannot find module → Check file extensions (.js for ESM) and module resolution
Slow compilation → Enable incremental, use skipLibCheck, consider esbuild/swc
→ See [troubleshooting.md](./references/troubleshooting.md) for detailed solutions with examples
Navigation
Detailed References
- [📐 Advanced Types](./references/advanced-types.md) - Conditional types, mapped types, infer keyword, recursive types. Load when building complex type utilities or generic libraries.
- [⚙️ Configuration](./references/configuration.md) - Complete tsconfig.json guide, project references, monorepo patterns. Load when setting up new projects or optimizing builds.
- [🔒 Runtime Validation](./references/runtime-validation.md) - Zod, TypeBox, Valibot deep patterns, error handling, integration strategies. Load when implementing API validation or form handling.
- [✨ Advanced Patterns 2025](./references/advanced-patterns-2025.md) - TypeScript 5.2+ features:
usingkeyword, stable decorators, import type behavior, satisfies with generics. Load when using modern language features.
- [🌳 Decision Trees](./references/decision-trees.md) - Clear decision frameworks for
typevsinterface, generics vs unions,unknownvsany, validation library selection, type narrowing strategies, and module resolution. Load when making TypeScript design decisions.
- [🔧 Troubleshooting](./references/troubleshooting.md) - Common TypeScript errors and fixes, type inference issues, module resolution problems, tsconfig misconfigurations, build performance optimization, and type compatibility errors. Load when debugging TypeScript issues.
JavaScript / Runtime Quality Anti-Patterns
The type system catches type errors, but a class of JavaScript defects is runtime/AST- level and survives into emitted JS and plain-JS files. Watch for:
- Loose equality (
==/!=) — coercion bugs and auth flaws; use===/!==(accept
== null only when commented as "null or undefined").
- Dynamic code execution (
eval,new Function(str), stringsetTimeout) — injection
risk and unnecessary; use direct syntax.
- Mutating builtins (
Object/Array/Function.prototype) — pollutesfor…in,
breaks the whole runtime; extend or use free functions instead.
- Variable shadowing, `var` instead of `let`/`const`, **using functions before
declaration** — readability and "wrong variable" bugs.
- Logical OR in `switch` case labels (
case 1 || 2:only matches 1) — use stacked
case labels.
- Repetitive deep-member access — cache the resolved chain in a local (esp. DOM).
- Non-wrapped IIFEs, backslash multiline strings, `new Array()` — readability
and well-known traps; prefer wrapped IIFEs, template literals, and array literals.
See [JS/TS Quality Anti-Patterns](./references/js-quality-antipatterns.md) for each defect with compliant/non-compliant examples, severities, false-positive filters, and the equivalent ESLint/SonarSource rule. Derived from CAST Highlight JavaScript code quality indicators (https://doc.casthighlight.com/), cross-referenced to ESLint core rules and SonarSource RSPEC.
Red Flags
Stop and reconsider if:
- Using
anyinstead ofunknownfor external data - Casting with
aswithout runtime validation - Disabling strict mode for convenience
- Using
@ts-ignorewithout clear justification - Index access without
noUncheckedIndexedAccess
Integration with Other Skills
- nextjs-core: Type-safe Server Actions and route handlers
- nextjs-v16: Async API patterns and Cache Components typing
- mcp-builder: Zod schemas for MCP tool inputs
Related Skills
When using Core, these skills enhance your workflow:
- react: TypeScript with React: component typing, hooks, generics
- nextjs: TypeScript in Next.js: Server Components, Server Actions typing
- drizzle: Type-safe database queries with Drizzle ORM
- prisma: Prisma's generated TypeScript types for database schemas
[Full documentation available in these skills if deployed in your bundle]
{
"name": "typescript-core",
"version": "1.2.0",
"category": "toolchain",
"toolchain": "typescript",
"framework": null,
"tags": [
"typescript",
"types",
"validation",
"zod",
"typebox",
"tsconfig",
"decision-trees",
"troubleshooting",
"code-quality",
"anti-patterns"
],
"entry_point_tokens": 107,
"full_tokens": 24858,
"author": "claude-mpm-skills",
"license": "MIT",
"requires": [],
"updated": "2026-06-15",
"source_path": "toolchains/typescript/core/SKILL.md",
"repository": "https://github.com/bobmatnyc/claude-mpm-skills"
}
Advanced TypeScript Patterns (2025)
Modern TypeScript 5.2+ patterns including explicit resource management, stable decorators, and type-level programming.
Explicit Resource Management (TS 5.2+)
The using keyword provides automatic resource disposal, replacing manual cleanup patterns.
Basic Pattern
// Disposable resource interface
interface Disposable {
[Symbol.dispose](): void;
}
// File handle with automatic cleanup
class FileHandle implements Disposable {
constructor(private path: string) {
console.log(`Opening ${path}`);
}
write(data: string): void {
console.log(`Writing to ${this.path}: ${data}`);
}
[Symbol.dispose](): void {
console.log(`Closing ${this.path}`);
}
}
// Automatic disposal at scope end
function processFile() {
using file = new FileHandle("data.txt");
file.write("Hello");
// File automatically closed here, even if exception thrown
}Async Resource Management
interface AsyncDisposable {
[Symbol.asyncDispose](): Promise<void>;
}
class DatabaseConnection implements AsyncDisposable {
constructor(private connectionString: string) {}
async query(sql: string): Promise<any[]> {
// Execute query
return [];
}
async [Symbol.asyncDispose](): Promise<void> {
console.log("Closing database connection");
// Async cleanup
}
}
async function queryDatabase() {
await using db = new DatabaseConnection("postgres://...");
const results = await db.query("SELECT * FROM users");
// Connection automatically closed here
return results;
}Multiple Resources
async function processWithMultipleResources() {
await using db = new DatabaseConnection("postgres://...");
await using cache = new RedisConnection("redis://...");
using file = new FileHandle("output.txt");
// Use all resources
const data = await db.query("SELECT * FROM users");
await cache.set("users", data);
file.write(JSON.stringify(data));
// All disposed in reverse order: file, cache, db
}Real-World Pattern: Transaction Management
class Transaction implements AsyncDisposable {
constructor(private db: Database) {
this.db.beginTransaction();
}
async commit(): Promise<void> {
await this.db.commit();
}
async rollback(): Promise<void> {
await this.db.rollback();
}
async [Symbol.asyncDispose](): Promise<void> {
// Auto-rollback if not committed
if (!this.committed) {
await this.rollback();
}
}
private committed = false;
}
async function transferFunds(from: string, to: string, amount: number) {
await using tx = new Transaction(db);
await db.debit(from, amount);
await db.credit(to, amount);
await tx.commit();
// Auto-rollback if any error occurs before commit
}Stable Decorators (TS 5.0+)
TypeScript 5.0 ships stable decorators aligned with the TC39 proposal.
Method Decorators
// Logging decorator
function log(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const original = descriptor.value;
descriptor.value = async function(...args: any[]) {
console.log(`[${propertyKey}] Called with:`, args);
const result = await original.apply(this, args);
console.log(`[${propertyKey}] Returned:`, result);
return result;
};
return descriptor;
}
class UserService {
@log
async createUser(email: string, name: string) {
return { id: 123, email, name };
}
}Class Decorators
// Singleton decorator
function singleton<T extends { new(...args: any[]): {} }>(constructor: T) {
return class extends constructor {
private static instance: any;
constructor(...args: any[]) {
if ((constructor as any).instance) {
return (constructor as any).instance;
}
super(...args);
(constructor as any).instance = this;
}
};
}
@singleton
class DatabasePool {
constructor(public connectionString: string) {
console.log("Pool created");
}
}
const pool1 = new DatabasePool("postgres://...");
const pool2 = new DatabasePool("postgres://...");
console.log(pool1 === pool2); // trueProperty Decorators with Metadata
// Validation decorator
function validate(rules: { min?: number; max?: number; pattern?: RegExp }) {
return function(target: any, propertyKey: string) {
let value: any;
Object.defineProperty(target, propertyKey, {
get() { return value; },
set(newValue: any) {
if (rules.min !== undefined && newValue < rules.min) {
throw new Error(`${propertyKey} must be >= ${rules.min}`);
}
if (rules.max !== undefined && newValue > rules.max) {
throw new Error(`${propertyKey} must be <= ${rules.max}`);
}
if (rules.pattern && !rules.pattern.test(newValue)) {
throw new Error(`${propertyKey} must match ${rules.pattern}`);
}
value = newValue;
}
});
};
}
class User {
@validate({ min: 0, max: 120 })
age!: number;
@validate({ pattern: /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i })
email!: string;
}
const user = new User();
user.age = 25; // ✅ Valid
user.age = 150; // ❌ Error: age must be <= 120Decorator Factory Pattern
function retry(maxAttempts: number, delayMs: number) {
return function(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const original = descriptor.value;
descriptor.value = async function(...args: any[]) {
let lastError: Error;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await original.apply(this, args);
} catch (error) {
lastError = error as Error;
if (attempt < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
}
throw lastError!;
};
return descriptor;
};
}
class ApiClient {
@retry(3, 1000)
async fetchUser(id: string) {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error("Fetch failed");
return response.json();
}
}Import Type Behavior (TS 5.0+)
TypeScript 5.0 changes how type imports work with verbatimModuleSyntax.
Type-Only Imports
// Type-only import (erased at runtime)
import type { User } from "./types";
// Regular import (kept at runtime)
import { createUser } from "./user";
// Mixed import (AVOID - use separate imports)
import { type User, createUser } from "./user";verbatimModuleSyntax Enforcement
// tsconfig.json
{
"compilerOptions": {
"verbatimModuleSyntax": true // Enforces explicit type imports
}
}With this option:
// ❌ ERROR with verbatimModuleSyntax
import { User } from "./types"; // User is only a type
// ✅ CORRECT
import type { User } from "./types";
// ✅ CORRECT for values
import { createUser } from "./user";Type-Only Exports
// types.ts
export type User = {
id: string;
name: string;
};
export type { User as UserType }; // Re-export as type-only
// ❌ ERROR - can't export type as value
export { User }; // Fails with verbatimModuleSyntaxSatisfies with Generics
Advanced satisfies patterns for type narrowing with generics.
Generic Constraint with Inference
function createTypedConfig<const T extends Record<string, unknown>>(
config: T
): T {
return config;
}
const config = createTypedConfig({
api: {
baseUrl: "https://api.example.com",
timeout: 5000
},
features: {
darkMode: true,
betaAccess: false
}
} satisfies Record<string, unknown>);
// Inferred type preserves literals:
config.api.baseUrl; // Type: "https://api.example.com"
config.api.timeout; // Type: 5000Builder Pattern with Satisfies
type QueryBuilder<T> = {
where: (condition: Partial<T>) => QueryBuilder<T>;
select: <K extends keyof T>(...keys: K[]) => QueryBuilder<Pick<T, K>>;
execute: () => Promise<T[]>;
};
function query<T>(): QueryBuilder<T> {
return {
where: (condition) => query<T>(),
select: (...keys) => query() as any,
execute: async () => []
} satisfies QueryBuilder<T>;
}
type User = { id: string; name: string; email: string };
const users = await query<User>()
.where({ name: "Alice" })
.select("id", "email")
.execute();
// Type: Pick<User, "id" | "email">[]Branded Types with Satisfies
type Brand<T, B> = T & { __brand: B };
type UserId = Brand<string, "UserId">;
type Email = Brand<string, "Email">;
function createUserId(id: string): UserId {
return id as UserId;
}
function createEmail(email: string): Email {
if (!email.includes("@")) {
throw new Error("Invalid email");
}
return email as Email;
}
// Use satisfies to ensure correct brand
const userId = createUserId("user-123") satisfies UserId;
const email = createEmail("user@example.com") satisfies Email;
// ❌ Type error - can't assign Email to UserId
const wrongType: UserId = email;Type-Level Programming
Advanced compile-time computation with TypeScript's type system.
Recursive Type Utilities
// Deep readonly
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? DeepReadonly<T[P]>
: T[P];
};
// Deep partial
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object
? DeepPartial<T[P]>
: T[P];
};
// Deep required
type DeepRequired<T> = {
[P in keyof T]-?: T[P] extends object
? DeepRequired<T[P]>
: T[P];
};String Manipulation Types
// Convert string to camelCase
type CamelCase<S extends string> =
S extends `${infer First}_${infer Rest}`
? `${Lowercase<First>}${Capitalize<CamelCase<Rest>>}`
: Lowercase<S>;
type Test1 = CamelCase<"user_name">; // "userName"
type Test2 = CamelCase<"api_base_url">; // "apiBaseUrl"
// Extract path parameters
type ExtractPathParams<T extends string> =
T extends `${infer Start}:${infer Param}/${infer Rest}`
? { [K in Param | keyof ExtractPathParams<Rest>]: string }
: T extends `${infer Start}:${infer Param}`
? { [K in Param]: string }
: {};
type Params = ExtractPathParams<"/users/:userId/posts/:postId">;
// { userId: string; postId: string }Conditional Type Inference
// Unwrap Promise type
type Awaited<T> =
T extends Promise<infer U> ? Awaited<U> : T;
type Test1 = Awaited<Promise<string>>; // string
type Test2 = Awaited<Promise<Promise<number>>>; // number
// Extract function return type
type ReturnTypeOf<T> =
T extends (...args: any[]) => infer R ? R : never;
async function fetchUser() { return { id: 1, name: "Alice" }; }
type User = Awaited<ReturnTypeOf<typeof fetchUser>>;
// { id: number; name: string }When to Use These Patterns
Use using When:
- Managing file handles, database connections, locks
- Implementing transactional logic
- Ensuring cleanup even with exceptions
- Working with Node.js streams
Use Decorators When:
- Cross-cutting concerns (logging, validation, caching)
- Framework integration (NestJS, TypeORM)
- Metadata-driven programming
- AOP (Aspect-Oriented Programming) patterns
Use satisfies with Generics When:
- Building type-safe builders/fluent APIs
- Creating branded types
- Narrowing types while preserving literals
- Library API design requiring inference
Use Type-Level Programming When:
- Building utility type libraries
- Transforming API types automatically
- Generating types from runtime values
- Advanced generic constraints
Red Flags
Stop and reconsider if:
- Using
usingfor non-resource objects (just use regular cleanup) - Creating decorators without understanding execution order
- Over-engineering with type-level programming (keep it simple)
- Using branded types without validation functions
- Type utilities so complex that error messages are unreadable
Advanced TypeScript Types
Deep patterns for type system mastery: conditional types, mapped types, inference, and recursive types.
Conditional Types
Basic Syntax
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // falseDistributive Behavior
Conditional types distribute over unions:
type ToArray<T> = T extends unknown ? T[] : never;
type Result = ToArray<string | number>; // string[] | number[]
// NOT (string | number)[]Prevent distribution with tuple wrapping:
type ToArrayNonDistributive<T> = [T] extends [unknown] ? T[] : never;
type Result = ToArrayNonDistributive<string | number>; // (string | number)[]The infer Keyword
Extract types from complex structures:
// Extract return type
type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never;
// Extract array element type
type ElementOf<T> = T extends (infer E)[] ? E : never;
// Extract Promise resolution type
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
// Extract function parameters
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
// Multiple infer positions
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;Practical Conditional Types
// Make all properties optional recursively
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
// Make all properties required recursively
type DeepRequired<T> = T extends object
? { [K in keyof T]-?: DeepRequired<T[K]> }
: T;
// Extract only function properties
type FunctionProperties<T> = {
[K in keyof T]: T[K] extends Function ? K : never
}[keyof T];
// Remove null/undefined from all properties
type NonNullableProperties<T> = {
[K in keyof T]: NonNullable<T[K]>
};Mapped Types
Basic Transformation
type Readonly<T> = { readonly [K in keyof T]: T[K] };
type Partial<T> = { [K in keyof T]?: T[K] };
type Required<T> = { [K in keyof T]-?: T[K] };
type Mutable<T> = { -readonly [K in keyof T]: T[K] };Key Remapping (as clause)
// Rename keys
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};
type User = { name: string; age: number };
type UserGetters = Getters<User>;
// { getName: () => string; getAge: () => number }
// Filter keys
type OnlyStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K]
};
// Exclude specific keys
type OmitByType<T, U> = {
[K in keyof T as T[K] extends U ? never : K]: T[K]
};Combining with Template Literals
type EventHandlers<T> = {
[K in keyof T as `on${Capitalize<string & K>}Change`]: (value: T[K]) => void
};
type Form = { name: string; email: string };
type FormHandlers = EventHandlers<Form>;
// { onNameChange: (value: string) => void; onEmailChange: (value: string) => void }Template Literal Types
String Manipulation
type Uppercase<S extends string> = intrinsic;
type Lowercase<S extends string> = intrinsic;
type Capitalize<S extends string> = intrinsic;
type Uncapitalize<S extends string> = intrinsic;
// Pattern matching
type ExtractDomain<T extends string> =
T extends `${infer Protocol}://${infer Domain}/${infer Path}`
? Domain
: never;
type Domain = ExtractDomain<"https://example.com/path">; // "example.com"Route Parameter Extraction
type ExtractRouteParams<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractRouteParams<`/${Rest}`>
: T extends `${string}:${infer Param}`
? Param
: never;
type Params = ExtractRouteParams<"/users/:userId/posts/:postId">;
// "userId" | "postId"
// Create params object type
type RouteParams<T extends string> = {
[K in ExtractRouteParams<T>]: string
};
type UserPostParams = RouteParams<"/users/:userId/posts/:postId">;
// { userId: string; postId: string }Event Name Patterns
type EventName<T extends string> = `${T}:${
| 'start'
| 'end'
| 'error'
| 'progress'
}`;
type FileEvents = EventName<'upload' | 'download'>;
// "upload:start" | "upload:end" | "upload:error" | "upload:progress" |
// "download:start" | "download:end" | "download:error" | "download:progress"Recursive Types
JSON Type
type JSONValue =
| string
| number
| boolean
| null
| JSONValue[]
| { [key: string]: JSONValue };Deep Object Paths
type Paths<T, D extends number = 10> = [D] extends [never]
? never
: T extends object
? {
[K in keyof T]-?: K extends string | number
? `${K}` | `${K}.${Paths<T[K], Prev[D]>}`
: never;
}[keyof T]
: never;
type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
type User = {
name: string;
address: {
city: string;
zip: { code: string };
};
};
type UserPaths = Paths<User>;
// "name" | "address" | "address.city" | "address.zip" | "address.zip.code"Get Value by Path
type Get<T, P extends string> = P extends `${infer K}.${infer Rest}`
? K extends keyof T
? Get<T[K], Rest>
: never
: P extends keyof T
? T[P]
: never;
type City = Get<User, "address.city">; // string
type ZipCode = Get<User, "address.zip.code">; // stringUtility Type Implementations
Pick and Omit
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;Record
type Record<K extends keyof any, T> = { [P in K]: T };Extract and Exclude
type Extract<T, U> = T extends U ? T : never;
type Exclude<T, U> = T extends U ? never : T;NonNullable
type NonNullable<T> = T & {};
// Or: T extends null | undefined ? never : T;Parameters and ReturnType
type Parameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never;
type ReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : any;Type Guards
User-Defined Type Guards
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'email' in value
);
}
// Assertion functions
function assertIsUser(value: unknown): asserts value is User {
if (!isUser(value)) {
throw new Error('Not a user');
}
}Narrowing Patterns
// typeof narrowing
function process(value: string | number) {
if (typeof value === 'string') {
return value.toUpperCase(); // value is string
}
return value.toFixed(2); // value is number
}
// instanceof narrowing
function handleError(error: Error | string) {
if (error instanceof Error) {
return error.message;
}
return error;
}
// in operator narrowing
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ('swim' in animal) {
animal.swim();
} else {
animal.fly();
}
}Variance Annotations
TypeScript 4.7+ supports explicit variance:
// Covariant (output position) - use `out`
interface Producer<out T> {
produce(): T;
}
// Contravariant (input position) - use `in`
interface Consumer<in T> {
consume(value: T): void;
}
// Invariant - use both
interface Processor<in out T> {
process(value: T): T;
}Best Practices
1. Prefer `unknown` over `any` for truly unknown types 2. Use type guards instead of type assertions 3. Leverage inference - let TypeScript infer when possible 4. Add constraints to generics for better error messages 5. Document complex types with JSDoc comments 6. Test utility types with type-level tests using Expect<Equal<A, B>>
TypeScript Configuration Guide
Complete tsconfig.json reference for modern TypeScript projects.
2025 Recommended Configuration
General-Purpose Projects
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"verbatimModuleSyntax": true,
"isolatedModules": true,
"moduleDetection": "force",
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}Next.js Projects
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"noUncheckedIndexedAccess": true,
"plugins": [{ "name": "next" }],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}Library Projects
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2020"],
"strict": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "dist",
"rootDir": "src",
"isolatedModules": true,
"verbatimModuleSyntax": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}Local Repo Baselines (Examples)
CLI / Node CommonJS (ai-code-review)
{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"moduleResolution": "Node",
"strict": true,
"outDir": "dist",
"declaration": true,
"sourceMap": true,
"types": ["vitest/globals", "node"],
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*"],
"exclude": ["dist", "**/*.test.ts"]
}NodeNext Strict (smarterthings)
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"noUncheckedIndexedAccess": true
},
"include": ["src/**/*"],
"exclude": ["dist", "tests"]
}Next.js Bundler (matsuoka-com)
Use the Next.js example above with moduleResolution: "bundler" and noEmit: true for app builds.
Node.js 22+ Type Stripping
{
"compilerOptions": {
"target": "ESNext",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"erasableSyntaxOnly": true,
"verbatimModuleSyntax": true,
"noEmit": true
}
}Key Options Explained
Module System
| Option | When to Use |
|---|---|
"module": "NodeNext" | Node.js packages with ESM |
"module": "ESNext" | Bundled apps (Vite, Webpack) |
"module": "CommonJS" | Legacy Node.js packages |
| Resolution | When to Use |
|---|---|
"moduleResolution": "NodeNext" | Node.js ESM packages |
"moduleResolution": "bundler" | Apps using Vite/Webpack/esbuild |
"moduleResolution": "node" | Legacy CommonJS |
Strictness Options
{
"compilerOptions": {
// Core strict mode (enables all below)
"strict": true,
// Additional strictness (not in strict)
"noUncheckedIndexedAccess": true, // T | undefined for index access
"exactOptionalPropertyTypes": true, // Distinguish missing vs undefined
"noPropertyAccessFromIndexSignature": true, // Require bracket notation
"noImplicitOverride": true // Require override keyword
}
}What `strict: true` enables:
strictNullChecks:nullandundefinedare distinct typesstrictFunctionTypes: Strict function parameter checkingstrictBindCallApply: Strictbind,call,applymethodsstrictPropertyInitialization: Class properties must be initializednoImplicitAny: Error on implicitanynoImplicitThis: Error on implicitthisuseUnknownInCatchVariables: Catch variables areunknownalwaysStrict: Emit "use strict"
Import/Export
{
"compilerOptions": {
// Modern: explicit type imports required
"verbatimModuleSyntax": true,
// Legacy alternative (deprecated)
"importsNotUsedAsValues": "error",
"preserveValueImports": true
}
}With verbatimModuleSyntax:
// ✅ Correct
import type { User } from './types';
import { createUser } from './utils';
// ❌ Error - type-only import not marked
import { User } from './types';Output Options
{
"compilerOptions": {
"outDir": "dist", // Output directory
"rootDir": "src", // Source directory
"declaration": true, // Generate .d.ts files
"declarationMap": true, // Source maps for .d.ts
"sourceMap": true, // Generate .js.map
"inlineSources": true, // Include source in maps
"declarationDir": "types" // Separate types directory
}
}Path Mapping
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@utils/*": ["src/utils/*"],
"@types/*": ["src/types/*"]
}
}
}Note: Path mappings require bundler/runtime support (tsconfig-paths for Node.js).
Project References (Monorepos)
Root tsconfig.json
{
"files": [],
"references": [
{ "path": "packages/core" },
{ "path": "packages/cli" },
{ "path": "packages/web" }
]
}Package tsconfig.json
{
"compilerOptions": {
"composite": true,
"declaration": true,
"declarationMap": true,
"outDir": "dist",
"rootDir": "src"
},
"references": [
{ "path": "../core" }
]
}Build Commands
# Build all projects
tsc --build
# Build with watch
tsc --build --watch
# Clean build artifacts
tsc --build --clean
# Force rebuild
tsc --build --forceConfiguration Inheritance
Base Configuration
// tsconfig.base.json
{
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"moduleDetection": "force"
}
}Extending Base
// tsconfig.json
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"outDir": "dist"
},
"include": ["src"]
}Official Base Configs
npm install -D @tsconfig/node20 @tsconfig/strictest{
"extends": ["@tsconfig/node20/tsconfig.json", "@tsconfig/strictest/tsconfig.json"],
"compilerOptions": {
"outDir": "dist"
}
}Common Issues
Issue: Cannot find module
Cause: Module resolution mismatch
Fix:
{
"compilerOptions": {
"moduleResolution": "NodeNext", // Or "bundler" for bundled apps
"module": "NodeNext"
}
}Issue: Type-only imports being emitted
Cause: Missing verbatimModuleSyntax
Fix:
{
"compilerOptions": {
"verbatimModuleSyntax": true
}
}Then use:
import type { SomeType } from './types';Issue: Index access returns T instead of T | undefined
Cause: Missing noUncheckedIndexedAccess
Fix:
{
"compilerOptions": {
"noUncheckedIndexedAccess": true
}
}Issue: ESM/CJS interop problems
Cause: Incorrect module settings
Fix for ESM packages:
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext"
}
}And in package.json:
{
"type": "module"
}Issue: Slow type checking
Fixes:
{
"compilerOptions": {
"skipLibCheck": true, // Skip .d.ts checking
"incremental": true, // Incremental compilation
"tsBuildInfoFile": ".tsbuildinfo"
},
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}TypeScript 5.9 Features
Deferred Module Evaluation
// Module evaluated only when property accessed
import defer * as feature from "./some-feature.js";
// Module NOT loaded yet
console.log("Starting...");
// NOW module loads
console.log(feature.specialConstant);Improved Return Type Narrowing
function getLength(x: string | number[]) {
if (hasLength(x)) {
return x.length; // Better narrowing
}
return 0;
}
function hasLength(x: unknown): x is { length: number } {
return typeof x === 'object' && x !== null && 'length' in x;
}Validation Checklist
- [ ]
strict: trueenabled - [ ]
noUncheckedIndexedAccess: truefor array safety - [ ]
verbatimModuleSyntax: truefor explicit type imports - [ ]
skipLibCheck: truefor faster builds - [ ] Module resolution matches runtime (NodeNext/bundler)
- [ ] Path mappings have runtime support
- [ ]
composite: truefor project references
TypeScript Decision Trees
This guide helps you make critical TypeScript decisions through clear decision trees and selection criteria.
Type vs Interface
Decision Tree
Need to define a shape for an object?
│
├─ YES → Is it a public API/library type?
│ │
│ ├─ YES → Use `interface`
│ │ ✅ Better error messages
│ │ ✅ Declaration merging for extensibility
│ │ ✅ Conventional for public APIs
│ │
│ └─ NO → Need union types or mapped types?
│ │
│ ├─ YES → Use `type`
│ │ ✅ Supports unions, intersections, mapped types
│ │ ✅ More flexible type operations
│ │
│ └─ NO → Use `interface` (default for object shapes)
│ ✅ Slightly better performance
│ ✅ Can extend later if needed
│
└─ NO → Defining primitives, unions, or utilities?
└─ Use `type`
✅ Required for non-object typesWhen to Use interface
✅ Use `interface` when:
- Defining public API contracts
- Building libraries or shared types
- Need declaration merging for extensibility
- Defining simple object shapes
- Want clearer error messages
// ✅ Good: Public API
export interface User {
id: string;
email: string;
role: UserRole;
}
// ✅ Good: Extensible via declaration merging
interface CustomWindow extends Window {
myApp: AppInstance;
}
// ✅ Good: Clear object shape
interface UserConfig {
theme: 'light' | 'dark';
locale: string;
}When to Use type
✅ Use `type` when:
- Need union or intersection types
- Using mapped types or conditional types
- Defining utility types
- Working with primitive types
- Creating type aliases
// ✅ Good: Union types
type Status = 'pending' | 'success' | 'error';
// ✅ Good: Mapped types
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
// ✅ Good: Conditional types
type ApiResponse<T> = T extends { error: any }
? { success: false; error: string }
: { success: true; data: T };
// ✅ Good: Intersection types
type AuthenticatedUser = User & { token: string };Practical Examples
❌ Interface - Cannot use unions:
// ❌ Error: Interface can only extend object types
interface Result = Success | Error;✅ Type - Unions work:
// ✅ Correct
type Result = Success | Error;✅ Interface - Declaration merging:
// ✅ Augment existing types
interface Window {
customProperty: string;
}
interface Window {
anotherProperty: number;
}
// Both properties merge into Window---
Generics vs Union Types
Decision Tree
Need to represent multiple possible types?
│
├─ Types are related/similar and preserve structure?
│ │
│ ├─ YES → Use Generics
│ │ ✅ Type safety maintained
│ │ ✅ Return type matches input type
│ │ ✅ Reusable across different types
│ │
│ └─ NO → Fixed set of unrelated types?
│ └─ Use Union Types
│ ✅ Explicit allowed types
│ ✅ No type parameter needed
│
└─ Single operation accepts different inputs?
│
├─ Input type determines output type → Use Generics
└─ Output type is always same → Use Union TypesWhen to Use Generics
✅ Use generics when:
- Function output type depends on input type
- Building reusable data structures
- Type relationships must be preserved
- Creating type-safe utilities
// ✅ Good: Preserves type relationship
function identity<T>(value: T): T {
return value;
}
const num = identity(42); // Type: number
const str = identity("hello"); // Type: string
// ✅ Good: Type-safe data structure
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
}
const numberStack = new Stack<number>();
numberStack.push(1); // ✅ OK
numberStack.push("test"); // ❌ Error
// ✅ Good: Type-safe API wrapper
async function fetchData<T>(url: string): Promise<T> {
const response = await fetch(url);
return response.json();
}
const user = await fetchData<User>("/api/user");
// user is typed as UserWhen to Use Union Types
✅ Use union types when:
- Fixed set of known types
- Types are unrelated
- Exhaustive type checking needed
- Discriminated unions for state
// ✅ Good: Fixed set of types
type StringOrNumber = string | number;
function formatValue(value: StringOrNumber): string {
if (typeof value === "string") {
return value.toUpperCase();
}
return value.toFixed(2);
}
// ✅ Good: Discriminated unions
type ApiResult<T> =
| { success: true; data: T }
| { success: false; error: string };
function handleResult<T>(result: ApiResult<T>): T {
if (result.success) {
return result.data; // Type narrowed to success case
}
throw new Error(result.error); // Type narrowed to error case
}
// ✅ Good: Known states
type LoadingState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: unknown }
| { status: 'error'; error: string };Anti-Patterns
❌ Generics - Overuse when union would work:
// ❌ Bad: Generic not needed
function log<T extends string | number>(value: T): void {
console.log(value);
}
// ✅ Good: Union is simpler
function log(value: string | number): void {
console.log(value);
}❌ Union - Loses type relationship:
// ❌ Bad: Loses input/output relationship
function wrapArray(value: string | number): (string | number)[] {
return [value];
}
const result = wrapArray(42); // Type: (string | number)[]
// Lost knowledge that it's number[]
// ✅ Good: Preserves type
function wrapArray<T>(value: T): T[] {
return [value];
}
const result = wrapArray(42); // Type: number[]---
unknown vs any Usage Guide
Decision Tree
Dealing with external/unvalidated data?
│
├─ YES → Need type safety?
│ │
│ ├─ YES → Use `unknown`
│ │ ✅ Forces validation before use
│ │ ✅ Type-safe
│ │ ✅ Prevents runtime errors
│ │
│ └─ NO → Rapid prototyping/migration?
│ └─ Use `any` (temporarily)
│ ⚠️ Plan to replace with proper types
│ ⚠️ Document why `any` is used
│
└─ NO → Writing type utilities?
│
├─ Need to accept anything → Use `unknown`
└─ Need to disable type checking → Use `any` (rare)When to Use unknown
✅ Use `unknown` when:
- Validating external data (APIs, user input)
- Don't know the type ahead of time
- Building type-safe utilities
- Replacing
anyfor safety
// ✅ Good: API response validation
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const data: unknown = await response.json();
// Must validate before use
return UserSchema.parse(data); // Using Zod
}
// ✅ Good: Safe type guard
function isString(value: unknown): value is string {
return typeof value === "string";
}
function processValue(value: unknown): void {
if (isString(value)) {
console.log(value.toUpperCase()); // ✅ Safe
}
}
// ✅ Good: Error handling
try {
someOperation();
} catch (error: unknown) {
if (error instanceof Error) {
console.error(error.message);
} else {
console.error("Unknown error:", error);
}
}When to Use any
⚠️ Use `any` sparingly when:
- Migrating JavaScript to TypeScript (temporarily)
- Interacting with poorly-typed libraries
- Rapid prototyping (document for later cleanup)
- Intentionally opting out of type checking (rare)
// ⚠️ Acceptable: Migration phase
// TODO: Replace with proper types
function legacyFunction(input: any): any {
// Complex logic being migrated
return input.someMethod();
}
// ⚠️ Acceptable: Poorly-typed library
import PoorlyTypedLib from 'some-library';
const instance: any = new PoorlyTypedLib();
// ❌ Bad: Lazy typing
function process(data: any): void { // Should use unknown
console.log(data);
}Anti-Patterns
❌ Using `any` when `unknown` is safer:
// ❌ Bad: `any` defeats type safety
function parseJson(jsonString: string): any {
return JSON.parse(jsonString);
}
const data = parseJson('{"name": "test"}');
data.nonExistent.method(); // ❌ Runtime error, no compile error
// ✅ Good: `unknown` forces validation
function parseJson(jsonString: string): unknown {
return JSON.parse(jsonString);
}
const data = parseJson('{"name": "test"}');
data.nonExistent.method(); // ✅ Compile error
// Must validate first---
Validation Library Choice
Decision Tree
Need runtime validation for external data?
│
├─ What's your primary use case?
│ │
│ ├─ Full-stack TypeScript with tRPC
│ │ └─ Choose Zod
│ │ ✅ Best tRPC integration
│ │ ✅ Rich ecosystem
│ │ ✅ Excellent DX
│ │
│ ├─ Need OpenAPI/JSON Schema
│ │ └─ Choose TypeBox
│ │ ✅ Generates JSON Schema
│ │ ✅ ~10x faster than Zod
│ │ ✅ Fastify integration
│ │
│ ├─ Edge/serverless functions
│ │ └─ Choose Valibot
│ │ ✅ Smallest bundle (~1.4kB)
│ │ ✅ Tree-shakeable
│ │ ✅ ~2x faster than Zod
│ │
│ └─ General web apps, forms, APIs
│ └─ Choose Zod (default)
│ ✅ Most popular, mature
│ ✅ Great error messages
│ ✅ Rich ecosystem
│
└─ NO → Use TypeScript-only typesComparison Matrix
| Feature | Zod | TypeBox | Valibot |
|---|---|---|---|
| Bundle Size | ~13.5kB | ~8kB | ~1.4kB |
| Performance | Baseline | ~10x faster | ~2x faster |
| JSON Schema | ❌ | ✅ Native | ⚠️ Via adapter |
| tRPC Integration | ✅ First-class | ⚠️ Custom | ⚠️ Custom |
| Error Messages | ✅ Excellent | ✅ Good | ✅ Good |
| Tree-shaking | ⚠️ Partial | ✅ Full | ✅ Full |
| OpenAPI | ⚠️ Via plugin | ✅ Native | ❌ |
| Ecosystem | ✅ Large | ⚠️ Growing | ⚠️ Small |
When to Choose Zod
✅ Choose Zod when:
- Building full-stack TypeScript apps
- Using tRPC for type-safe APIs
- Need rich ecosystem (form libraries, etc.)
- Want excellent error messages
- Team is already familiar with Zod
import { z } from "zod";
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
age: z.number().int().positive().optional(),
role: z.enum(["admin", "user"]),
});
type User = z.infer<typeof UserSchema>;
// Validate
const user = UserSchema.parse(data);
// Safe parse (no throw)
const result = UserSchema.safeParse(data);
if (result.success) {
console.log(result.data);
} else {
console.error(result.error.format());
}Best for: Next.js, tRPC, React Hook Form, general web apps
When to Choose TypeBox
✅ Choose TypeBox when:
- Need OpenAPI/JSON Schema generation
- Performance is critical (high-throughput APIs)
- Using Fastify framework
- Need JSON Schema for external tools
import { Type, Static } from "@sinclair/typebox";
const UserSchema = Type.Object({
id: Type.String({ format: "uuid" }),
email: Type.String({ format: "email" }),
age: Type.Optional(Type.Integer({ minimum: 0 })),
role: Type.Union([
Type.Literal("admin"),
Type.Literal("user"),
]),
});
type User = Static<typeof UserSchema>;
// Generates JSON Schema
const jsonSchema = JSON.stringify(UserSchema);Best for: Fastify, OpenAPI documentation, JSON Schema tooling, performance-critical APIs
When to Choose Valibot
✅ Choose Valibot when:
- Bundle size is critical (edge functions)
- Building serverless/edge functions
- Need tree-shaking for minimal bundles
- Want faster validation than Zod
import * as v from "valibot";
const UserSchema = v.object({
id: v.pipe(v.string(), v.uuid()),
email: v.pipe(v.string(), v.email()),
age: v.optional(v.pipe(v.number(), v.integer(), v.minValue(0))),
role: v.picklist(["admin", "user"]),
});
type User = v.InferOutput<typeof UserSchema>;
// Validate
const user = v.parse(UserSchema, data);
// Safe parse
const result = v.safeParse(UserSchema, data);
if (result.success) {
console.log(result.output);
} else {
console.error(result.issues);
}Best for: Cloudflare Workers, Vercel Edge Functions, Deno Deploy, minimal bundles
Migration Path
If you need to switch libraries:
Zod → TypeBox: Use for performance, keep Zod for tRPC endpoints Zod → Valibot: Gradual migration, start with edge functions TypeBox → Zod: For better DX and ecosystem
---
Type Narrowing Strategy Selection
Decision Tree
Need to narrow a union type to specific case?
│
├─ Discriminated union with literal `type` field?
│ └─ Use Switch/If on discriminant
│ ✅ Exhaustiveness checking
│ ✅ Clearest intent
│
├─ Checking primitive type?
│ └─ Use `typeof` guard
│ ✅ Built-in JavaScript
│
├─ Checking class instance?
│ └─ Use `instanceof` guard
│ ✅ Prototype chain checking
│
├─ Custom logic needed?
│ └─ Use Type Predicate function
│ ✅ Reusable
│ ✅ Clear intent
│
└─ Complex validation?
└─ Use Assertion Function
✅ Throws on invalid
✅ Acts as guardDiscriminated Unions (Best Practice)
✅ Best approach for unions:
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "square"; size: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "square":
return shape.size ** 2;
default:
// Exhaustiveness check
const _exhaustive: never = shape;
throw new Error(`Unhandled shape: ${_exhaustive}`);
}
}Type Guards
`typeof` guards:
function processValue(value: string | number): string {
if (typeof value === "string") {
return value.toUpperCase(); // value: string
}
return value.toFixed(2); // value: number
}`instanceof` guards:
function handleError(error: Error | string): void {
if (error instanceof Error) {
console.error(error.message); // error: Error
} else {
console.error(error); // error: string
}
}Custom type predicates:
interface User {
id: string;
email: string;
}
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"email" in value
);
}
function processData(data: unknown): void {
if (isUser(data)) {
console.log(data.email); // data: User
}
}---
Module Resolution Strategy
Decision Tree
Starting new TypeScript project?
│
├─ Node.js project (not bundler)?
│ │
│ ├─ Node.js 20.6+ (native ESM support)?
│ │ └─ Use NodeNext
│ │ ✅ Modern Node.js resolution
│ │ ✅ ESM/CJS interop
│ │
│ └─ Older Node.js or CommonJS project?
│ └─ Use Node16 or Node
│ ✅ Traditional Node.js resolution
│
├─ Bundler (Vite, Webpack, esbuild)?
│ └─ Use Bundler
│ ✅ Simplified resolution
│ ✅ Trusts bundler
│
└─ Library/Package?
└─ Use NodeNext + "type": "module"
✅ Modern package standard
✅ Best compatibilityRecommended Settings (2025)
New Node.js projects:
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"verbatimModuleSyntax": true
}
}Bundler projects (Vite, Webpack):
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "Bundler",
"verbatimModuleSyntax": true
}
}Legacy projects:
{
"compilerOptions": {
"module": "CommonJS",
"moduleResolution": "Node"
}
}Module Resolution Comparison
| Strategy | Use Case | ESM Support | CJS Support |
|---|---|---|---|
| NodeNext | Modern Node.js | ✅ Native | ✅ Interop |
| Node16 | Node.js 16+ | ✅ Native | ✅ Interop |
| Node | Legacy Node.js | ⚠️ Via .mjs | ✅ Native |
| Bundler | Webpack/Vite | ✅ Yes | ✅ Yes |
---
Decision Checklist
Before making TypeScript choices, ask:
1. Is this a public API? → Prefer interface over type 2. Do I need type relationships? → Use generics, not unions 3. Is this external data? → Use unknown, not any 4. Need runtime validation? → Choose validation library based on use case 5. Is this a discriminated union? → Use switch on discriminant 6. What's my module system? → Choose appropriate resolution strategy
---
Related References
- [Advanced Types](./advanced-types.md) - Conditional types, mapped types, recursive types
- [Configuration](./configuration.md) - Complete tsconfig.json guide
- [Runtime Validation](./runtime-validation.md) - Deep dive into Zod, TypeBox, Valibot
- [Troubleshooting](./troubleshooting.md) - Common TypeScript issues and fixes
JavaScript / TypeScript Quality Anti-Patterns
Overview
TypeScript's type system catches type errors, but a large class of JavaScript defects are runtime/AST-level issues the compiler does not flag: scope leaks, mutation of builtins, loose equality, dynamic code execution, and readability traps. These survive into emitted JS and into plain-JS portions of a codebase. This reference collects the high-signal ones with compliant/non-compliant examples, severities, and false-positive filters.
Source note: These anti-patterns are derived from CAST Highlight's JavaScript code
quality indicators (https://doc.casthighlight.com/), paraphrased with original examples.
Where CAST defers to open standards, the primary source is cited: SonarSource RSPEC
(https://rules.sonarsource.com/) and the ESLint core rules
(https://eslint.org/docs/latest/rules/). Severities are guidance for review triage, not
CAST's proprietary calibration.
Severity legend: HIGH = correctness/security risk; MEDIUM = reliability/maintenance risk; LOW = consistency/readability. Apply the 80% confidence filter — flag only when the pattern is clearly the defect, not a deliberate, commented exception.
---
1. Loose equality / implied typecasting (== vs ===)
Severity: HIGH (Resiliency / Security)
== and != coerce operands before comparing, producing surprising truthiness (0 == "", null == undefined, "\t\n" == 0) that leads to data-handling bugs and, on auth/authorization paths, security flaws. Use strict === / !==.
Non-compliant:
if (userInput == 0) grantAccess(); // "" , "0x0", false, [] all coerce to 0
if (role != null) { /* also true for undefined — sometimes unintended */ }Compliant:
if (userInput === 0) grantAccess();
if (role !== null && role !== undefined) { /* explicit */ }
// or, when "null or undefined" is genuinely intended, document it:
if (role == null) { /* intentional: null OR undefined */ } // commented exceptionFalse-positive filter: x == null is an idiomatic, widely-accepted shorthand for "null or undefined." Flag the other loose comparisons; accept == null when commented or clearly intentional. Mirrors ESLint eqeqeq (with "smart" allowing == null) and SonarSource RSPEC.
---
2. Dynamic code execution (eval, new Function, string setTimeout)
Severity: HIGH (Security)
eval() (and new Function(str), setTimeout("code", …)) executes arbitrary strings as code. If any part of the string is influenced by input, it is an injection vector; even when "safe," it defeats the optimizer and is almost never necessary — arithmetic and property access have direct syntax.
Non-compliant:
const result = eval(`${a} + ${b}`); // injectable, slow, unnecessary
const fn = new Function("return " + expr); // same familyCompliant:
const result = a + b;
const value = obj[dynamicKey]; // dynamic property access, no evalFalse-positive filter: Genuine, sandboxed interpreters (a math-expression evaluator library, a controlled plugin host) are out of scope — but should never use raw eval. Mirrors ESLint no-eval / no-implied-eval.
---
3. Modifying builtin objects (Object/Array/Function.prototype)
Severity: HIGH (Reliability)
Mutating Object.prototype, Array.prototype, or Function.prototype breaks assumptions across the entire runtime — notably it pollutes for…in and the object-as-hash-table pattern, producing bugs that are extremely hard to trace because the cause is in a different module.
Non-compliant:
Object.prototype.toMap = function () { /* ... */ }; // pollutes every for…in
Array.prototype.last = function () { return this[this.length - 1]; };Compliant:
function lastOf(arr) { return arr[arr.length - 1]; } // free function
class TypedMap extends Map { /* extend, don't mutate builtins */ }False-positive filter: Well-known, scoped polyfills that conditionally add a standard method when absent (if (!Array.prototype.flat) { … }) are acceptable. Flag mutation that adds non-standard members to builtins. Mirrors ESLint no-extend-native.
---
4. Variable shadowing
Severity: MEDIUM (Changeability / Security)
An inner declaration that reuses an outer name silently hides the outer binding, making it easy to read or write the wrong variable. It is a frequent source of "why didn't my change take effect" bugs.
Non-compliant:
const items = getItems();
list.forEach((items) => { // shadows outer `items`
process(items); // which `items`? confusing
});Compliant:
const items = getItems();
list.forEach((item) => {
process(item);
});False-positive filter: Short, conventional callback params in tiny scopes are low-risk; flag shadowing that spans a non-trivial body or shadows a module-level binding. Mirrors ESLint no-shadow.
---
5. Use let/const, never var (and prefer const)
Severity: MEDIUM (Changeability)
var is function-scoped and hoisted, leaking out of blocks and enabling declare-after-use. const/let are block-scoped and express mutability intent.
Non-compliant:
for (var i = 0; i < n; i++) { /* ... */ }
console.log(i); // `i` leaks past the loopCompliant:
for (let i = 0; i < n; i++) { /* ... */ }
const TOTAL = computeTotal(); // const for never-reassigned bindingsFalse-positive filter: None worth keeping in modern code — var in new TS/JS is essentially always a finding. Legacy files migrating incrementally are the only context to defer. Mirrors ESLint no-var / prefer-const.
---
6. Logical OR in switch case labels
Severity: MEDIUM (Reliability)
case 1 || 2: does not match 1 or 2 — 1 || 2 evaluates to 1, so only 1 is handled and 2 silently falls to default. The intent is expressed with stacked case labels (fall-through).
Non-compliant:
switch (x) {
case 1 || 2: // only matches 1; `2` hits default
doSomething(x); break;
default:
boom(); // fires for x === 2, unexpectedly
}Compliant:
switch (x) {
case 1:
case 2: // intentional fall-through groups both
doSomething(x); break;
default:
boom();
}False-positive filter: None — a logical operator in a case label is always the bug. Mirrors SonarSource RSPEC-3616.
---
7. Repetitive access to deep nested members
Severity: MEDIUM (Efficiency / Elegance)
Re-resolving a deep member chain (window.location.href, config.a.b.c.d, document.querySelector(...)) on every use forces the engine to walk the resolution path — and for DOM access this is genuinely expensive. Cache the resolved value in a local when read more than once in a scope.
Non-compliant:
if (config.services.auth.tokens.refresh.enabled) {
schedule(config.services.auth.tokens.refresh.ttl); // path walked twice
}Compliant:
const refresh = config.services.auth.tokens.refresh;
if (refresh.enabled) {
schedule(refresh.ttl);
}False-positive filter: A path read once, or reads separated by a mutation that could change the value, are not violations. Flag the same path read 2+ times with no intervening write. (See also code-review-standards Efficiency criterion 3 — greedy data access.)
---
8. Non-wrapped immediately-invoked function expressions (IIFE)
Severity: LOW (Changeability)
Wrap an immediately-invoked function in parentheses so the reader sees the value is the result of the call, not the function itself, and to avoid parser ambiguity.
Non-compliant:
const config = function () { return load(); }(); // ambiguous to readers/parsersCompliant:
const config = (function () { return load(); })();
// modern: just use an arrow or a named function — IIFEs are rarely needed with modules
const config = (() => load())();False-positive filter: With ES modules, IIFEs are largely obsolete; prefer module scope. Flag the un-wrapped form when an IIFE is genuinely used. Mirrors ESLint wrap-iife.
---
9. Multiline string literals via backslash line-continuation
Severity: LOW (Reliability)
A \ at end of line to continue a string is not part of ECMAScript proper, and trailing whitespace after the \ causes tricky, invisible errors. Use template literals or string concatenation.
Non-compliant:
const msg = 'a long message \
that continues'; // whitespace after `\` breaks silentlyCompliant:
const msg = `a long message
that continues`; // template literal
const msg2 = 'a long message ' +
'that continues'; // explicit concatenationFalse-positive filter: None — prefer template literals. Mirrors SonarSource RSPEC-3616.
---
10. Array literals over new Array()
Severity: LOW (Reliability)
new Array(3) does not create [3] — it creates an array of length 3 with no elements, a well-known trap. The literal syntax is shorter and unambiguous.
Non-compliant:
const a = new Array(1, 2, 3); // works, but verbose
const b = new Array(3); // length 3, no elements — surprisingCompliant:
const a = [1, 2, 3];
const b = Array.from({ length: 3 }, () => 0); // explicit when you want fixed lengthFalse-positive filter: Array.from / Array.of for intentional length/iterable construction are fine. Flag new Array(...). Mirrors ESLint no-array-constructor.
---
11. Using functions before their declaration
Severity: LOW (Changeability)
Hoisting lets you call a function declaration before it appears, but readers scan top-to-bottom; calling before declaring forces them to scroll to understand. Declare before use (and with const-bound arrow functions, hoisting won't save you anyway).
Non-compliant:
render(); // works via hoisting, but reads backwards
function render() { /* ... */ }Compliant:
function render() { /* ... */ }
render();False-positive filter: Mutually-recursive functions and intentional declaration-at-bottom modules are acceptable when consistent. Mirrors ESLint no-use-before-define.
---
How these map to scoring
In the code-quality-scoring skill's Software Health model: items 1–3, 6, 9, 10 feed Resiliency; items 4, 5, 8, 11 feed Agility; item 7 feeds Elegance. Counting these at scale gives a per-dimension signal, not just a fix list.
References
- CAST Highlight JavaScript code quality indicators — https://doc.casthighlight.com/
- ESLint core rules — https://eslint.org/docs/latest/rules/
- SonarSource RSPEC (TypeScript/JavaScript) — https://rules.sonarsource.com/
Runtime Validation in TypeScript
Deep patterns for Zod, TypeBox, and Valibot with error handling and integration strategies.
Library Selection Guide
Decision Matrix
| Requirement | Best Choice |
|---|---|
| Full-stack with tRPC | Zod |
| OpenAPI/JSON Schema generation | TypeBox |
| Edge/serverless (bundle size critical) | Valibot |
| Maximum validation speed | TypeBox (compiled) |
| Largest ecosystem/integrations | Zod |
Bundle Size Comparison
Zod: ~13.5kB minified
TypeBox: ~8kB minified
Valibot: ~1.4kB minified (tree-shakeable)Performance Comparison
TypeBox (compiled): 10x baseline
Valibot: 2x baseline
Zod: 1x baseline (reference)Zod Deep Patterns
Schema Composition
import { z } from 'zod';
// Base schemas
const EmailSchema = z.string().email();
const UUIDSchema = z.string().uuid();
const TimestampSchema = z.string().datetime();
// Compose into larger schemas
const BaseEntity = z.object({
id: UUIDSchema,
createdAt: TimestampSchema,
updatedAt: TimestampSchema,
});
const UserSchema = BaseEntity.extend({
email: EmailSchema,
name: z.string().min(1).max(100),
role: z.enum(['admin', 'user', 'guest']),
});
// Infer types
type User = z.infer<typeof UserSchema>;Transformations
// Transform during parsing
const DateSchema = z.string().datetime().transform(s => new Date(s));
// Coercion (convert types)
const NumberFromString = z.coerce.number();
const BooleanFromString = z.coerce.boolean();
// Complex transformation
const APIResponseSchema = z.object({
data: z.array(z.object({
id: z.string(),
created_at: z.string(),
})),
}).transform(response => ({
items: response.data.map(item => ({
id: item.id,
createdAt: new Date(item.created_at),
})),
}));Refinements and Superrefine
// Simple refinement
const PasswordSchema = z.string()
.min(8)
.refine(
(val) => /[A-Z]/.test(val),
{ message: 'Must contain uppercase letter' }
)
.refine(
(val) => /[0-9]/.test(val),
{ message: 'Must contain number' }
);
// Superrefine for multiple errors
const FormSchema = z.object({
password: z.string(),
confirmPassword: z.string(),
}).superRefine((data, ctx) => {
if (data.password !== data.confirmPassword) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Passwords must match',
path: ['confirmPassword'],
});
}
});
// Async refinement
const UniqueEmailSchema = z.string().email().refine(
async (email) => {
const exists = await checkEmailExists(email);
return !exists;
},
{ message: 'Email already registered' }
);Discriminated Unions
const EventSchema = z.discriminatedUnion('type', [
z.object({
type: z.literal('click'),
x: z.number(),
y: z.number(),
}),
z.object({
type: z.literal('keypress'),
key: z.string(),
}),
z.object({
type: z.literal('scroll'),
direction: z.enum(['up', 'down']),
delta: z.number(),
}),
]);
type Event = z.infer<typeof EventSchema>;Error Handling
// Safe parse with detailed errors
function validateUser(input: unknown) {
const result = UserSchema.safeParse(input);
if (!result.success) {
// Format errors for API response
const errors = result.error.issues.map(issue => ({
field: issue.path.join('.'),
message: issue.message,
code: issue.code,
}));
return { success: false as const, errors };
}
return { success: true as const, data: result.data };
}
// Custom error map
const customErrorMap: z.ZodErrorMap = (issue, ctx) => {
if (issue.code === z.ZodIssueCode.invalid_type) {
if (issue.expected === 'string') {
return { message: 'This field must be text' };
}
}
return { message: ctx.defaultError };
};
z.setErrorMap(customErrorMap);Zod with Forms
// React Hook Form integration
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
const FormSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
type FormData = z.infer<typeof FormSchema>;
function SignupForm() {
const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
resolver: zodResolver(FormSchema),
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
{/* ... */}
</form>
);
}TypeBox Deep Patterns
Schema Definition
import { Type, Static } from '@sinclair/typebox';
// Basic types
const StringType = Type.String();
const NumberType = Type.Number();
const BooleanType = Type.Boolean();
// With constraints
const EmailType = Type.String({ format: 'email' });
const PositiveNumber = Type.Number({ minimum: 0 });
const BoundedString = Type.String({ minLength: 1, maxLength: 100 });
// Object schema
const UserSchema = Type.Object({
id: Type.String({ format: 'uuid' }),
email: Type.String({ format: 'email' }),
name: Type.String({ minLength: 1 }),
age: Type.Optional(Type.Number({ minimum: 0 })),
role: Type.Union([
Type.Literal('admin'),
Type.Literal('user'),
]),
});
type User = Static<typeof UserSchema>;Compiled Validation (10x Speed)
import { TypeCompiler } from '@sinclair/typebox/compiler';
const CompiledUser = TypeCompiler.Compile(UserSchema);
// Check (returns boolean)
if (CompiledUser.Check(input)) {
// input is User
}
// Errors (returns iterator)
const errors = [...CompiledUser.Errors(input)];
// Decode (returns value or throws)
const user = CompiledUser.Decode(input);JSON Schema Output
// TypeBox schemas ARE JSON Schema
const jsonSchema = UserSchema;
console.log(JSON.stringify(jsonSchema, null, 2));
// {
// "type": "object",
// "properties": {
// "id": { "type": "string", "format": "uuid" },
// "email": { "type": "string", "format": "email" },
// ...
// },
// "required": ["id", "email", "name", "role"]
// }Fastify Integration
import Fastify from 'fastify';
import { Type } from '@sinclair/typebox';
import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox';
const server = Fastify().withTypeProvider<TypeBoxTypeProvider>();
const CreateUserBody = Type.Object({
email: Type.String({ format: 'email' }),
name: Type.String(),
});
const UserResponse = Type.Object({
id: Type.String(),
email: Type.String(),
name: Type.String(),
});
server.post('/users', {
schema: {
body: CreateUserBody,
response: { 200: UserResponse },
},
}, async (request, reply) => {
// request.body is typed as { email: string; name: string }
const user = await createUser(request.body);
return user;
});Transformations with Value Module
import { Value } from '@sinclair/typebox/value';
// Clean extra properties
const cleaned = Value.Clean(UserSchema, input);
// Convert types
const converted = Value.Convert(UserSchema, { age: '25' });
// { age: 25 }
// Default values
const SchemaWithDefaults = Type.Object({
name: Type.String({ default: 'Anonymous' }),
count: Type.Number({ default: 0 }),
});
const withDefaults = Value.Default(SchemaWithDefaults, {});
// { name: 'Anonymous', count: 0 }Valibot Deep Patterns
Schema Definition
import * as v from 'valibot';
// Basic schemas
const StringSchema = v.string();
const NumberSchema = v.number();
const BooleanSchema = v.boolean();
// With validations
const EmailSchema = v.pipe(v.string(), v.email());
const PositiveSchema = v.pipe(v.number(), v.minValue(0));
// Object schema
const UserSchema = v.object({
id: v.pipe(v.string(), v.uuid()),
email: v.pipe(v.string(), v.email()),
name: v.pipe(v.string(), v.minLength(1)),
age: v.optional(v.pipe(v.number(), v.minValue(0))),
role: v.union([v.literal('admin'), v.literal('user')]),
});
type User = v.InferOutput<typeof UserSchema>;Pipe Pattern
// Build complex schemas with pipes
const PasswordSchema = v.pipe(
v.string(),
v.minLength(8, 'Minimum 8 characters'),
v.maxLength(100, 'Maximum 100 characters'),
v.regex(/[A-Z]/, 'Must contain uppercase'),
v.regex(/[0-9]/, 'Must contain number'),
);
// Transform
const DateSchema = v.pipe(
v.string(),
v.isoDateTime(),
v.transform((s) => new Date(s)),
);Error Handling
// Safe parse
const result = v.safeParse(UserSchema, input);
if (result.success) {
const user = result.output;
} else {
const errors = v.flatten(result.issues);
// { nested: { email: ['Invalid email'] } }
}
// Parse (throws)
try {
const user = v.parse(UserSchema, input);
} catch (error) {
if (error instanceof v.ValiError) {
console.log(error.issues);
}
}Tree-Shaking Advantage
// Only imports what you use
import { string, email, parse } from 'valibot';
// vs Zod which imports everything
import { z } from 'zod';Integration Patterns
API Validation Middleware
// Generic validation middleware (works with any library)
import { z } from 'zod';
import { NextResponse } from 'next/server';
function validateBody<T>(schema: z.ZodSchema<T>) {
return async (request: Request): Promise<T> => {
const body = await request.json();
const result = schema.safeParse(body);
if (!result.success) {
throw new ValidationError(result.error);
}
return result.data;
};
}
// Usage in route handler
const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string(),
});
export async function POST(request: Request) {
try {
const body = await validateBody(CreateUserSchema)(request);
const user = await createUser(body);
return NextResponse.json(user);
} catch (error) {
if (error instanceof ValidationError) {
return NextResponse.json(
{ errors: error.format() },
{ status: 400 }
);
}
throw error;
}
}Environment Variable Validation
import { z } from 'zod';
const envSchema = z.object({
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
NODE_ENV: z.enum(['development', 'production', 'test']),
PORT: z.coerce.number().default(3000),
});
// Validate at startup
export const env = envSchema.parse(process.env);
// Type-safe environment access
declare global {
namespace NodeJS {
interface ProcessEnv extends z.infer<typeof envSchema> {}
}
}Database Schema Sync
// Shared schema between validation and database
import { z } from 'zod';
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
import { createInsertSchema, createSelectSchema } from 'drizzle-zod';
// Drizzle schema
const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
email: text('email').notNull().unique(),
name: text('name').notNull(),
createdAt: timestamp('created_at').defaultNow(),
});
// Auto-generate Zod schemas from Drizzle
const insertUserSchema = createInsertSchema(users, {
email: z.string().email(),
name: z.string().min(1).max(100),
});
const selectUserSchema = createSelectSchema(users);
type InsertUser = z.infer<typeof insertUserSchema>;
type User = z.infer<typeof selectUserSchema>;Best Practices
1. Define schemas once, derive types - Never duplicate type definitions 2. Validate at boundaries - API routes, form submissions, external data 3. Use safe parse - Handle errors gracefully instead of throwing 4. Compose schemas - Build complex schemas from simple ones 5. Test schemas - Validate edge cases and error messages 6. Consider bundle size - Use Valibot for edge/serverless 7. Use compiled validation - TypeBox compiler for hot paths
TypeScript Troubleshooting Guide
Comprehensive troubleshooting guide for common TypeScript errors, build issues, and configuration problems.
Table of Contents
- Common TypeScript Errors
- Type Inference Issues
- Module Resolution Problems
- tsconfig.json Misconfigurations
- Build Performance Issues
- Type Compatibility Errors
---
Common TypeScript Errors
TS2339: Property does not exist on type
Problem:
const user = { name: "Alice" };
console.log(user.age); // ❌ Property 'age' does not exist on type '{ name: string; }'Diagnosis:
- TypeScript inferred a narrow type
- Property was accessed before being added
- Object type doesn't include the property
Solutions:
Solution 1: Define proper interface
interface User {
name: string;
age?: number; // Optional property
}
const user: User = { name: "Alice" };
console.log(user.age); // ✅ OK (age is optional)Solution 2: Type assertion (use sparingly)
const user = { name: "Alice" } as any;
console.log(user.age); // ⚠️ Works but defeats type safety
// Better: Use unknown and validate
const user: unknown = getData();
if (isUser(user)) {
console.log(user.age); // ✅ Type-safe
}Solution 3: Index signature for dynamic properties
interface User {
name: string;
[key: string]: unknown; // Allow arbitrary properties
}---
TS2345: Argument is not assignable to parameter
Problem:
function greet(name: string): void {
console.log(`Hello, ${name}`);
}
greet(123); // ❌ Argument of type 'number' is not assignable to parameter of type 'string'Diagnosis:
- Type mismatch between argument and parameter
- Implicit
anywas converted to explicit type - Function overload not matching
Solutions:
Solution 1: Fix the argument type
greet("Alice"); // ✅ Correct type
greet(String(123)); // ✅ Convert to stringSolution 2: Make function more flexible
function greet(name: string | number): void {
console.log(`Hello, ${String(name)}`);
}
greet(123); // ✅ OK
greet("Alice"); // ✅ OKSolution 3: Use generics for type preservation
function identity<T>(value: T): T {
return value;
}
const num = identity(123); // Type: number
const str = identity("test"); // Type: string---
TS2322: Type is not assignable to type
Problem:
interface Config {
apiUrl: string;
timeout: number;
}
const config: Config = {
apiUrl: "https://api.example.com",
timeout: "5000" // ❌ Type 'string' is not assignable to type 'number'
};Diagnosis:
- Property has wrong type
- Missing required properties
- Extra properties not allowed
Solutions:
Solution 1: Fix property type
const config: Config = {
apiUrl: "https://api.example.com",
timeout: 5000 // ✅ Correct type
};Solution 2: Use type assertion (if you're certain)
const config = {
apiUrl: "https://api.example.com",
timeout: "5000"
} as Config; // ⚠️ Bypasses type checkingSolution 3: Use Zod for runtime validation
import { z } from "zod";
const ConfigSchema = z.object({
apiUrl: z.string().url(),
timeout: z.number().int().positive()
});
// This will throw at runtime if types are wrong
const config = ConfigSchema.parse(data);---
TS2554: Expected X arguments, but got Y
Problem:
function add(a: number, b: number): number {
return a + b;
}
add(5); // ❌ Expected 2 arguments, but got 1Diagnosis:
- Missing required parameters
- Optional parameters confused with required
- Destructuring issues
Solutions:
Solution 1: Provide all required arguments
add(5, 10); // ✅ OKSolution 2: Make parameters optional
function add(a: number, b: number = 0): number {
return a + b;
}
add(5); // ✅ OK, b defaults to 0Solution 3: Use rest parameters
function sum(...numbers: number[]): number {
return numbers.reduce((acc, n) => acc + n, 0);
}
sum(5); // ✅ OK
sum(5, 10, 15); // ✅ OK---
TS2339: Property does not exist on Window
Problem:
window.myGlobal = "test"; // ❌ Property 'myGlobal' does not exist on type 'Window & typeof globalThis'Diagnosis:
- Adding custom properties to global objects
- TypeScript doesn't know about custom globals
Solutions:
Solution 1: Extend Window interface
// types/global.d.ts
declare global {
interface Window {
myGlobal: string;
}
}
export {}; // Make this a module
// Now this works
window.myGlobal = "test"; // ✅ OKSolution 2: Use type assertion (quick fix)
(window as any).myGlobal = "test"; // ⚠️ Works but not type-safe---
TS18048: Object is possibly 'undefined'
Problem:
const users = [{ name: "Alice" }];
console.log(users.find(u => u.name === "Bob").name);
// ❌ Object is possibly 'undefined'Diagnosis:
- Accessing property on potentially undefined value
- Array methods like
find()can returnundefined - Enabled
strictNullChecksornoUncheckedIndexedAccess
Solutions:
Solution 1: Optional chaining
console.log(users.find(u => u.name === "Bob")?.name); // ✅ OKSolution 2: Nullish coalescing
const user = users.find(u => u.name === "Bob") ?? { name: "Unknown" };
console.log(user.name); // ✅ OKSolution 3: Type guard
const user = users.find(u => u.name === "Bob");
if (user) {
console.log(user.name); // ✅ OK, narrowed to non-undefined
}Solution 4: Non-null assertion (use with caution)
console.log(users.find(u => u.name === "Bob")!.name);
// ⚠️ Asserts non-null, runtime error if actually undefined---
Type Inference Issues
Issue: TypeScript infers wrong type
Problem:
const config = {
apiUrl: "https://api.example.com",
retryCount: 3
};
// Later...
config.apiUrl = "https://new-api.example.com"; // ✅ OK
config.retryCount = "5"; // ❌ Type 'string' is not assignable to type 'number'
// TypeScript inferred: { apiUrl: string; retryCount: number }Diagnosis:
- TypeScript inferred mutable object type
- Wanted literal types or stricter inference
Solutions:
Solution 1: Use `as const` for literal types
const config = {
apiUrl: "https://api.example.com",
retryCount: 3
} as const;
// Type: { readonly apiUrl: "https://api.example.com"; readonly retryCount: 3 }Solution 2: Use `satisfies` to validate without widening
type Config = {
apiUrl: string;
retryCount: number;
};
const config = {
apiUrl: "https://api.example.com",
retryCount: 3
} satisfies Config;
// Type: { apiUrl: "https://api.example.com"; retryCount: 3 }
// Validates structure while preserving literalsSolution 3: Explicit type annotation
const config: Config = {
apiUrl: "https://api.example.com",
retryCount: 3
};
// Type: Config---
Issue: Generic type not inferred correctly
Problem:
function createArray<T>(items: T[]): T[] {
return items;
}
const result = createArray([]); // Type: never[]Diagnosis:
- TypeScript can't infer generic from empty array
- Need explicit type parameter
Solutions:
Solution 1: Provide type parameter explicitly
const result = createArray<number>([]); // Type: number[]Solution 2: Pass non-empty array
const result = createArray([1, 2, 3]); // Type: number[]Solution 3: Use default type parameter
function createArray<T = unknown>(items: T[]): T[] {
return items;
}
const result = createArray([]); // Type: unknown[]---
Module Resolution Problems
Issue: Cannot find module 'X'
Problem:
import { User } from './types'; // ❌ Cannot find module './types'Diagnosis:
- Missing file extension
- Incorrect path
- Module resolution strategy mismatch
Solutions:
Solution 1: Add file extension (ESM with NodeNext)
import { User } from './types.js'; // ✅ Note: .js, not .tsSolution 2: Check tsconfig module resolution
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext"
}
}Solution 3: Use path mapping
{
"compilerOptions": {
"baseUrl": "./src",
"paths": {
"@types/*": ["types/*"]
}
}
}import { User } from '@types/user'; // ✅ Uses path mapping---
Issue: Module has no default export
Problem:
import config from './config'; // ❌ Module has no default exportDiagnosis:
- File uses named exports, not default export
- Trying to import as default
Solutions:
Solution 1: Use named import
import { config } from './config'; // ✅ Named importSolution 2: Import everything as namespace
import * as Config from './config'; // ✅ Namespace import
Config.config;Solution 3: Add default export to module
// config.ts
export const config = { /* ... */ };
export default config; // Add default export---
Issue: ESM/CommonJS interop problems
Problem:
// Using NodeNext with .mts file
const express = require('express'); // ❌ require is not definedDiagnosis:
- Mixing ESM and CommonJS syntax
- File extension determines module system
Solutions:
Solution 1: Use ESM syntax
import express from 'express'; // ✅ ESM importSolution 2: Use CommonJS file (.cts)
// file.cts
const express = require('express'); // ✅ OK in .cts filesSolution 3: Dynamic import for ESM
const express = await import('express'); // ✅ Dynamic ESM import---
tsconfig.json Misconfigurations
Issue: Strict mode errors everywhere
Problem: After enabling "strict": true, thousands of errors appear.
Diagnosis:
- Migrating from loose to strict mode
- Code written without strict checking
Solutions:
Solution 1: Gradual strict mode adoption
{
"compilerOptions": {
"strict": false,
"noImplicitAny": true, // Enable one at a time
"strictNullChecks": false,
"strictFunctionTypes": false
}
}Solution 2: Use `@ts-expect-error` for migration
// @ts-expect-error - TODO: Fix this type error
const result = legacyFunction(data);Solution 3: Fix gradually by file/directory
{
"compilerOptions": {
"strict": true
},
"exclude": [
"src/legacy/**/*" // Exclude legacy code temporarily
]
}---
Issue: Index access unsafe with noUncheckedIndexedAccess
Problem:
// tsconfig: "noUncheckedIndexedAccess": true
const users = ["Alice", "Bob"];
console.log(users[0].toUpperCase());
// ❌ Object is possibly 'undefined'Diagnosis:
noUncheckedIndexedAccessmakes index access returnT | undefined- Good for safety, but requires null checks
Solutions:
Solution 1: Add null check
const user = users[0];
if (user) {
console.log(user.toUpperCase()); // ✅ OK
}Solution 2: Use optional chaining
console.log(users[0]?.toUpperCase()); // ✅ OKSolution 3: Use Array methods instead
users.forEach(user => {
console.log(user.toUpperCase()); // ✅ OK, user is never undefined
});---
Issue: Cannot use JSX without proper configuration
Problem:
const element = <div>Hello</div>; // ❌ Cannot use JSX unless the '--jsx' flag is providedDiagnosis:
- Missing JSX configuration in tsconfig
Solutions:
Solution 1: Configure JSX for React
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "react"
}
}Solution 2: Use legacy React JSX
{
"compilerOptions": {
"jsx": "react"
}
}Solution 3: Preserve JSX for other tools
{
"compilerOptions": {
"jsx": "preserve" // For Next.js, SWC, etc.
}
}---
Build Performance Issues
Issue: TypeScript compilation is slow
Problem: tsc takes 30+ seconds on medium-sized project.
Diagnosis:
- Inefficient tsconfig settings
- Unnecessary file scanning
- Missing incremental compilation
Solutions:
Solution 1: Enable incremental compilation
{
"compilerOptions": {
"incremental": true,
"tsBuildInfoFile": ".tsbuildinfo"
}
}Solution 2: Use project references for monorepos
{
"compilerOptions": {
"composite": true,
"declaration": true
},
"references": [
{ "path": "../shared" }
]
}Solution 3: Optimize includes/excludes
{
"include": ["src/**/*"],
"exclude": [
"node_modules",
"dist",
"**/*.test.ts",
"**/*.spec.ts"
]
}Solution 4: Skip lib checking
{
"compilerOptions": {
"skipLibCheck": true // Skip type checking of .d.ts files
}
}Solution 5: Use faster alternatives
- esbuild: ~100x faster for bundling
- swc: ~20x faster for transpilation
- Vite: Uses esbuild for dev builds
---
Issue: High memory usage during compilation
Problem: TypeScript compiler uses 4GB+ RAM.
Diagnosis:
- Large project with many files
- Type checking entire node_modules
Solutions:
Solution 1: Increase Node.js memory
node --max-old-space-size=8192 node_modules/.bin/tscSolution 2: Use skipLibCheck
{
"compilerOptions": {
"skipLibCheck": true
}
}Solution 3: Split into smaller projects Use TypeScript project references to split codebase.
---
Type Compatibility Errors
Issue: Structural typing allows unexpected assignments
Problem:
interface User {
id: string;
name: string;
}
interface Product {
id: string;
name: string;
}
const user: User = { id: "1", name: "Alice" };
const product: Product = user; // ✅ No error, but conceptually wrongDiagnosis:
- TypeScript uses structural typing (duck typing)
- Two interfaces with same structure are compatible
Solutions:
Solution 1: Nominal typing with branding
interface User {
id: string;
name: string;
_brand: "User"; // Nominal brand
}
interface Product {
id: string;
name: string;
_brand: "Product"; // Different brand
}
const user: User = { id: "1", name: "Alice", _brand: "User" };
const product: Product = user; // ❌ Error: brands don't matchSolution 2: Use classes for nominal typing
class User {
constructor(public id: string, public name: string) {}
}
class Product {
constructor(public id: string, public name: string) {}
}
const user = new User("1", "Alice");
const product: Product = user; // ❌ Error: different classes---
Issue: Discriminated union not narrowing
Problem:
type Result =
| { success: true; data: string }
| { success: false; error: string };
function handle(result: Result) {
if (result.success) {
console.log(result.data); // ❌ Property 'data' does not exist
}
}Diagnosis:
successproperty is not a literal type- TypeScript can't narrow without literal discriminant
Solutions:
Solution 1: Use literal types
type Result =
| { success: true; data: string } // Literal true
| { success: false; error: string }; // Literal false
function handle(result: Result) {
if (result.success === true) { // Explicit literal check
console.log(result.data); // ✅ OK
}
}Solution 2: Use `as const` in object creation
const successResult = {
success: true as const,
data: "Hello"
};---
Quick Diagnostic Checklist
When encountering TypeScript errors:
1. Read the full error message - TypeScript errors are verbose but accurate 2. Check tsconfig.json - Many issues stem from configuration 3. Verify imports - Ensure correct paths and extensions 4. Check strictness flags - Know which strict modes are enabled 5. Use IDE hover - See inferred types by hovering in VS Code 6. Simplify - Create minimal reproduction to isolate issue 7. Search TypeScript issues - Many edge cases documented on GitHub 8. Check TypeScript version - Features and behaviors change between versions
---
Related References
- [Decision Trees](./decision-trees.md) - Make better TypeScript design decisions
- [Configuration](./configuration.md) - Complete tsconfig.json reference
- [Advanced Types](./advanced-types.md) - Deep dive into complex type patterns
- [Runtime Validation](./runtime-validation.md) - Zod, TypeBox, Valibot patterns
Related skills
FAQ
What TypeScript topics does typescript-core cover?
typescript-core covers TypeScript types, generics, control-flow narrowing, module organization, and strict compiler options. The skill helps developers author and refactor TS codebases with safer inference instead of relying on loose `any` escapes.
Does typescript-core replace framework-specific skills?
typescript-core focuses on core language mechanics—types, generics, narrowing, modules, and strict tsconfig—that underpin React, Node, and shared packages. Framework layout, routing, and component patterns still belong in framework-specific skills.