
Typescript Magician
- 1 installs
- Updated July 11, 2026
- dimmageiras/lazy_days_playground
This is a copy of typescript-magician by mcollina - installs and ranking accrue to the original listing.
Helps with ai & agent building tasks.
About
typescript-magician is a Claude Code skill for ai & agent building. It helps developers move faster with AI-assisted coding.
- typescript-magician
- AI & Agent Building
- AI-coding skill
Typescript Magician by the numbers
- 1 all-time installs (skills.sh)
- Data as of Jul 12, 2026 (Skillselion catalog sync)
npx skills add https://github.com/dimmageiras/lazy_days_playground --skill typescript-magicianAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| Last updated | July 11, 2026 |
| Repository | dimmageiras/lazy_days_playground ↗ |
What it does
Helps with ai & agent building tasks.
Files
When to use
Use this skill for:
- TypeScript errors and type challenges
- Eliminating
anytypes from codebases - Complex generics and type inference issues
- When strict typing is needed
Instructions
When invoked:
1. Run tsc --noEmit to capture the full error output before making changes 2. Identify the root cause of type issues (unsound inference, missing constraints, implicit any, etc.) 3. Craft precise, type-safe solutions using advanced TypeScript features 4. Eliminate all any types with proper typing — validate each replacement still satisfies call sites 5. Confirm the fix compiles cleanly with a second tsc --noEmit pass
Capabilities include:
- Advanced generics and conditional types
- Template literal types and mapped types
- Utility types and type manipulation
- Brand types and nominal typing
- Complex inference patterns
- Variance and distribution rules
- Module augmentation and declaration merging
For every TypeScript challenge:
- Explain the type theory behind the problem
- Provide multiple solution approaches when applicable
- Show before/after type representations
- Include comprehensive type tests
- Ensure full IntelliSense support
Quick Examples
Eliminating any with generics
Before
function getProperty(obj: any, key: string): any {
return obj[key];
}After
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
// getProperty({ name: "Alice" }, "name") → inferred as string ✓Narrowing an unknown API response
Before
async function fetchUser(): Promise<any> {
const res = await fetch("/api/user");
return res.json();
}After
interface User {
id: number;
name: string;
}
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"name" in value
);
}
async function fetchUser(): Promise<User> {
const res = await fetch("/api/user");
const data: unknown = await res.json();
if (!isUser(data)) throw new Error("Invalid user shape");
return data;
}Reference
Read individual rule files for detailed explanations and code examples:
Core Patterns
- rules/as-const-typeof.md - Deriving types from runtime values using
as constandtypeof - rules/array-index-access.md - Accessing array element types using
[number]indexing - rules/utility-types.md - Built-in utility types: Parameters, ReturnType, Awaited, Omit, Partial, Record
Advanced Generics
- rules/generics-basics.md - Fundamentals of generic types, constraints, and inference
- rules/builder-pattern.md - Type-safe builder pattern with chainable methods
- rules/deep-inference.md - Achieving deep type inference with F.Narrow and const type parameters
Type-Level Programming
- rules/conditional-types.md - Conditional types for type-level if/else logic
- rules/infer-keyword.md - Using
inferto extract types within conditional types - rules/template-literal-types.md - String manipulation at the type level
- rules/mapped-types.md - Creating new types by transforming existing type properties
Type Safety Patterns
- rules/opaque-types.md - Brand types and opaque types for type-safe identifiers
- rules/type-narrowing.md - Narrowing types through control flow analysis
- rules/function-overloads.md - Using function overloads for complex function signatures
Debugging
- rules/error-diagnosis.md - Strategies for diagnosing and understanding TypeScript type errors
Array Index Access with [number]
Overview
In TypeScript, you can access array element types using indexed access types. The [number] syntax is particularly powerful for extracting a union of all possible element types from an array or tuple.
Basic Concept
Just like you can access object properties with string keys, you can access array elements with numeric indices:
const roles = ["user", "admin", "anonymous"] as const;
// Access specific index
type FirstRole = typeof roles[0]; // "user"
type SecondRole = typeof roles[1]; // "admin"
// Access all elements with [number]
type AnyRole = typeof roles[number]; // "user" | "admin" | "anonymous"Why [number] Works
The number type, when used as an index, represents a union of ALL possible numeric indices. TypeScript uses this as a shortcut to access all elements:
// This is conceptually equivalent to:
type AnyRole = typeof roles[0 | 1 | 2];
// But [number] handles any array length automaticallyPattern: Extract Array Element Types
const userAccessModel = {
user: ["update-self", "view"],
admin: ["create", "update-self", "update-any", "delete", "view"],
anonymous: ["view"],
} as const;
type Role = keyof typeof userAccessModel;
// Type: "user" | "admin" | "anonymous"
// Get all values (arrays) as a union
type UserAccessModelValues = typeof userAccessModel[Role];
// Type: readonly ["update-self", "view"] | readonly ["create", ...] | readonly ["view"]
// Get all actions from all roles
type Action = typeof userAccessModel[Role][number];
// Type: "update-self" | "view" | "create" | "update-any" | "delete"Difference Between Tuple and Array Access
// Tuple - fixed length, specific types at each position
const tuple = ["hello", 42, true] as const;
type TupleElements = typeof tuple[number]; // "hello" | 42 | true
// Array - variable length, single element type
const array: string[] = ["a", "b", "c"];
type ArrayElement = typeof array[number]; // stringPattern: Extract Function Parameter Types
Combined with Parameters<>, you can get a union of all parameter types:
const funcWithManyParameters = (
a: string,
b: string,
c: number,
d: boolean,
) => {
return [a, b, c, d].join(" ");
};
// Get tuple of all parameter types
type ParamsTuple = Parameters<typeof funcWithManyParameters>;
// Type: [string, string, number, boolean]
// Get union of all parameter types
type ParamsUnion = Parameters<typeof funcWithManyParameters>[number];
// Type: string | number | booleanWhen to Use This Pattern
- Role-based access control: Extract all possible actions/permissions
- Configuration validation: Get all possible config values
- Event systems: Extract all possible event types from an array
- Form fields: Get all field names from a fields array
Practical Example: Type-Safe Access Control
const userAccessModel = {
user: ["update-self", "view"],
admin: ["create", "update-self", "update-any", "delete", "view"],
anonymous: ["view"],
} as const;
type Role = keyof typeof userAccessModel;
type Action = typeof userAccessModel[Role][number];
const canUserAccess = (role: Role, action: Action): boolean => {
// Need to cast because TypeScript can't narrow the array type
return (userAccessModel[role] as ReadonlyArray<Action>).includes(action);
};
// Type-safe usage
canUserAccess("admin", "delete"); // OK
canUserAccess("user", "delete"); // OK at compile time, false at runtime
canUserAccess("admin", "invalid"); // Error: "invalid" is not assignable to ActionCommon Pitfalls
Forgetting as const on Arrays
// BAD - elements widened to string
const actions = ["view", "edit", "delete"];
type Action = typeof actions[number]; // string
// GOOD - literal types preserved
const actions = ["view", "edit", "delete"] as const;
type Action = typeof actions[number]; // "view" | "edit" | "delete"ReadonlyArray Type Mismatch
When using .includes() on readonly arrays, you may need to cast:
const items = ["a", "b", "c"] as const;
type Item = typeof items[number];
// Error: Argument of type 'string' is not assignable to parameter of type '"a" | "b" | "c"'
const hasItem = items.includes(someString);
// Solution: Cast the array
const hasItem = (items as ReadonlyArray<Item>).includes(value as Item);Advanced: Conditional Access
You can combine [number] with conditional types:
type ExtractArrayElements<T> = T extends readonly (infer U)[] ? U : never;
const permissions = ["read", "write", "admin"] as const;
type Permission = ExtractArrayElements<typeof permissions>;
// Type: "read" | "write" | "admin"Deriving Types from Runtime with as const and typeof
Overview
The combination of as const and typeof is one of the most powerful patterns in TypeScript for deriving types from runtime values. This pattern allows you to create a single source of truth that works at both runtime and compile time.
The as const Assertion
as const is a const assertion that makes an object deeply readonly and infers literal types instead of widened types.
// Without as const - types are widened
const config = {
GROUP: "group",
ANNOUNCEMENT: "announcement",
};
// Type: { GROUP: string; ANNOUNCEMENT: string }
// With as const - literal types are preserved
const config = {
GROUP: "group",
ANNOUNCEMENT: "announcement",
} as const;
// Type: { readonly GROUP: "group"; readonly ANNOUNCEMENT: "announcement" }Key Benefits of as const
1. Immutability: Properties become readonly recursively 2. Literal inference: Values are inferred as their literal types, not widened types 3. Array tuple inference: Arrays become readonly tuples with literal types
const routes = ["home", "about", "contact"] as const;
// Type: readonly ["home", "about", "contact"]
// Without as const: string[]Pulling Runtime to Type World with typeof
Use typeof to extract the type from a runtime value:
const programModeEnumMap = {
GROUP: "group",
ANNOUNCEMENT: "announcement",
ONE_ON_ONE: "1on1",
SELF_DIRECTED: "selfDirected",
} as const;
// Extract the type of the object
type ProgramMap = typeof programModeEnumMap;
// Extract keys as a union type
type BackendProgram = keyof typeof programModeEnumMap;
// Type: "GROUP" | "ANNOUNCEMENT" | "ONE_ON_ONE" | "SELF_DIRECTED"
// Extract values as a union type using indexed access
type FrontendProgram = typeof programModeEnumMap[keyof typeof programModeEnumMap];
// Type: "group" | "announcement" | "1on1" | "selfDirected"Pattern: Obj[keyof Obj] for Object Values
This pattern is like Object.values() for the type world:
const statusCodes = {
OK: 200,
CREATED: 201,
BAD_REQUEST: 400,
NOT_FOUND: 404,
} as const;
type StatusCode = typeof statusCodes[keyof typeof statusCodes];
// Type: 200 | 201 | 400 | 404Pattern: Subset Selection with Union Index
You can select a subset of values by using a union of specific keys:
const programModeEnumMap = {
GROUP: "group",
ANNOUNCEMENT: "announcement",
ONE_ON_ONE: "1on1",
SELF_DIRECTED: "selfDirected",
} as const;
type ProgramMap = typeof programModeEnumMap;
// Select only individual program types
type IndividualProgram = ProgramMap["ONE_ON_ONE" | "SELF_DIRECTED"];
// Type: "1on1" | "selfDirected"When to Use This Pattern
- Configuration objects: Define config once, use it at runtime and compile time
- Enum alternatives: Create type-safe enums with string/number values
- Route definitions: Define routes with their metadata
- API mappings: Map between different representations (e.g., backend vs frontend)
- Event types: Define event names and their payloads
Common Pitfalls
Forgetting as const
Without as const, you lose literal type inference:
// BAD - values are widened to string
const colors = {
RED: "#ff0000",
GREEN: "#00ff00",
};
type Color = typeof colors[keyof typeof colors]; // string
// GOOD - literal types preserved
const colors = {
RED: "#ff0000",
GREEN: "#00ff00",
} as const;
type Color = typeof colors[keyof typeof colors]; // "#ff0000" | "#00ff00"Attempting to Mutate
as const makes objects readonly - attempting to mutate will cause a compile error:
const config = {
timeout: 5000,
} as const;
config.timeout = 10000; // Error: Cannot assign to 'timeout' because it is a read-only propertyComplete Example
// Single source of truth for HTTP methods
const HTTP_METHODS = {
GET: "GET",
POST: "POST",
PUT: "PUT",
DELETE: "DELETE",
PATCH: "PATCH",
} as const;
type HttpMethod = typeof HTTP_METHODS[keyof typeof HTTP_METHODS];
// Type: "GET" | "POST" | "PUT" | "DELETE" | "PATCH"
type SafeMethod = typeof HTTP_METHODS["GET"];
// Type: "GET"
type MutatingMethod = typeof HTTP_METHODS["POST" | "PUT" | "DELETE" | "PATCH"];
// Type: "POST" | "PUT" | "DELETE" | "PATCH"
function makeRequest(method: HttpMethod, url: string): void {
// method is type-safe
}
makeRequest(HTTP_METHODS.GET, "/api/users"); // OK
makeRequest("INVALID", "/api/users"); // ErrorType-Safe Builder Pattern
Overview
The builder pattern uses a chain of method calls to incrementally build up a data structure or configuration. With TypeScript, we can make this pattern fully type-safe, tracking accumulated state at the type level.
Basic Concept
Each method returns a new or modified builder with updated type information:
new DbSeeder()
.addUser("matt", { name: "Matt" })
.addPost("post1", { title: "Hello" })
.transact();
// Each step updates the type to include what was addedImplementation Pattern
Step 1: Define the Base Types
interface User {
id: string;
name: string;
}
interface Post {
id: string;
title: string;
authorId: string;
}
// Shape that constrains our generic
interface DbShape {
users: Record<string, User>;
posts: Record<string, Post>;
}Step 2: Create the Generic Builder
export class DbSeeder<TDatabase extends DbShape> {
public users: DbShape["users"] = {};
public posts: DbShape["posts"] = {};
// Each method returns DbSeeder with EXTENDED type information
addUser = <Id extends string>(
id: Id,
user: Omit<User, "id">,
): DbSeeder<TDatabase & { users: TDatabase["users"] & Record<Id, User> }> => {
this.users[id] = { ...user, id };
return this;
};
addPost = <Id extends string>(
id: Id,
post: Omit<Post, "id">,
): DbSeeder<TDatabase & { posts: TDatabase["posts"] & Record<Id, Post> }> => {
this.posts[id] = { ...post, id };
return this;
};
// Final method returns the built result with correct types
transact = async () => {
// Actual database operations would go here
return {
users: this.users as TDatabase["users"],
posts: this.posts as TDatabase["posts"],
};
};
}Step 3: Usage with Type Inference
const usage = async () => {
const result = await new DbSeeder()
.addUser("matt", { name: "Matt" })
.addPost("post1", { authorId: "matt", title: "Hello" })
.addPost("post2", { authorId: "matt", title: "World" })
.transact();
// result.users.matt is typed as User
// result.posts.post1 is typed as Post
// result.posts.post2 is typed as Post
console.log(result.users.matt.name); // Type-safe!
console.log(result.posts.post1.title); // Type-safe!
};How the Types Build Up
Each method call extends the type:
new DbSeeder()
// Type: DbSeeder<{ users: {}; posts: {} }>
.addUser("matt", { name: "Matt" })
// Type: DbSeeder<{ users: Record<"matt", User>; posts: {} }>
.addPost("post1", { ... })
// Type: DbSeeder<{ users: Record<"matt", User>; posts: Record<"post1", Post> }>
.addPost("post2", { ... })
// Type: DbSeeder<{ users: Record<"matt", User>; posts: Record<"post1" | "post2", Post> }>Key Techniques
1. Generic ID Capture
Capture literal types by using a generic with string constraint:
addUser = <Id extends string>(
id: Id, // Id is inferred as literal type "matt", not string
user: Omit<User, "id">,
): DbSeeder<TDatabase & { users: TDatabase["users"] & Record<Id, User> }>2. Intersection for Type Accumulation
Use & to add new type information while preserving existing:
TDatabase & { users: TDatabase["users"] & Record<Id, User> }3. Cast in Terminal Methods
The runtime types don't match compile-time types, so cast in the final method:
transact = async () => {
return {
users: this.users as TDatabase["users"],
posts: this.posts as TDatabase["posts"],
};
};Pattern: Query Builder
interface QueryState {
table: string | null;
columns: string[];
whereClause: string | null;
}
class QueryBuilder<TState extends QueryState> {
private state: TState;
private constructor(state: TState) {
this.state = state;
}
static create() {
return new QueryBuilder({
table: null,
columns: [],
whereClause: null,
});
}
from<T extends string>(
table: T
): QueryBuilder<TState & { table: T }> {
return new QueryBuilder({ ...this.state, table });
}
select<C extends string[]>(
...columns: C
): QueryBuilder<TState & { columns: C }> {
return new QueryBuilder({ ...this.state, columns });
}
where<W extends string>(
clause: W
): QueryBuilder<TState & { whereClause: W }> {
return new QueryBuilder({ ...this.state, whereClause: clause });
}
// Only allow build if table is set
build(this: QueryBuilder<TState & { table: string }>): string {
const cols = this.state.columns.length
? this.state.columns.join(", ")
: "*";
let sql = `SELECT ${cols} FROM ${this.state.table}`;
if (this.state.whereClause) {
sql += ` WHERE ${this.state.whereClause}`;
}
return sql;
}
}
// Usage
const query = QueryBuilder.create()
.from("users")
.select("id", "name")
.where("active = true")
.build();
// Error: Can't build without from()
QueryBuilder.create().select("id").build(); // Type error!Pattern: Configuration Builder with Required Fields
interface ServerConfig {
host: string;
port: number;
ssl?: boolean;
timeout?: number;
}
type RequiredFields = "host" | "port";
type ConfiguredFields<T> = { [K in keyof T]-?: K };
class ConfigBuilder<TConfigured extends Partial<Record<keyof ServerConfig, true>>> {
private config: Partial<ServerConfig> = {};
host(value: string): ConfigBuilder<TConfigured & { host: true }> {
this.config.host = value;
return this as any;
}
port(value: number): ConfigBuilder<TConfigured & { port: true }> {
this.config.port = value;
return this as any;
}
ssl(value: boolean): ConfigBuilder<TConfigured & { ssl: true }> {
this.config.ssl = value;
return this as any;
}
// Only allow build when required fields are set
build(
this: ConfigBuilder<{ host: true; port: true }>
): ServerConfig {
return this.config as ServerConfig;
}
}
// Usage
const config = new ConfigBuilder()
.host("localhost")
.port(3000)
.ssl(true)
.build();
// Error: Missing required fields
new ConfigBuilder().host("localhost").build(); // Type error!Advanced: Default Values
export class DbSeeder<
TDatabase extends DbShape = {
users: { defaultUser: User };
posts: {};
}
> {
public users: DbShape["users"] = {
defaultUser: { id: "default", name: "Default User" },
};
// ...
}
// Now every DbSeeder starts with defaultUser
const seeder = new DbSeeder();
// seeder has users.defaultUser by defaultWhen to Use Builder Pattern
- Complex object construction: Many optional/required fields
- Fluent APIs: DSLs for queries, configurations, test data
- Validation at type level: Ensure required steps are completed
- Incremental building: Add pieces over time before finalizing
Common Pitfalls
Forgetting to Constrain the Generic
// BAD - TDatabase could be anything
class DbSeeder<TDatabase> {
// Error: Cannot access TDatabase["users"]
}
// GOOD - constrained to DbShape
class DbSeeder<TDatabase extends DbShape> {
// Can safely access TDatabase["users"] and TDatabase["posts"]
}Not Casting in Terminal Methods
// BAD - type mismatch
transact = async () => {
return {
users: this.users, // Type: Record<string, User>, not TDatabase["users"]
posts: this.posts,
};
};
// GOOD - cast to match accumulated type
transact = async () => {
return {
users: this.users as TDatabase["users"],
posts: this.posts as TDatabase["posts"],
};
};Returning this Instead of New Type
// BAD - returns same type, loses type information
addUser(id: string, user: Omit<User, "id">): this {
return this;
}
// GOOD - returns new generic instantiation
addUser<Id extends string>(
id: Id,
user: Omit<User, "id">,
): DbSeeder<TDatabase & { users: TDatabase["users"] & Record<Id, User> }> {
return this;
}Conditional Types
Overview
Conditional types provide if/else logic at the type level. They use the extends keyword to check type relationships and return different types based on the result.
Basic Syntax
type Conditional = SomeType extends OtherType ? TrueType : FalseType;The condition checks if SomeType is assignable to OtherType.
Simple Examples
// Check if type is string
type IsString<T> = T extends string ? true : false;
type Test1 = IsString<string>; // true
type Test2 = IsString<number>; // false
type Test3 = IsString<"hello">; // true (literal extends string)
// Check type relationships
type Result1 = string extends string ? "yes" : "no"; // "yes"
type Result2 = string extends number ? "yes" : "no"; // "no"
type Result3 = "hello" extends string ? "yes" : "no"; // "yes"Practical Example: Null Checking
type IsNullable<T> = null extends T ? true : false;
type Test1 = IsNullable<string | null>; // true
type Test2 = IsNullable<string>; // false
type Test3 = IsNullable<undefined>; // false (null !== undefined)Conditional Types with Generics
// Return different types based on input
type TypeName<T> = T extends string
? "string"
: T extends number
? "number"
: T extends boolean
? "boolean"
: T extends undefined
? "undefined"
: T extends Function
? "function"
: "object";
type T1 = TypeName<string>; // "string"
type T2 = TypeName<number>; // "number"
type T3 = TypeName<() => void>; // "function"
type T4 = TypeName<{ a: 1 }>; // "object"Distribution Over Unions
When a conditional type acts on a union, it distributes over each member:
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>;
// Distributes to: ToArray<string> | ToArray<number>
// Result: string[] | number[]
// NOT: (string | number)[]Preventing Distribution
Wrap in tuple to prevent distribution:
type ToArrayNonDistributive<T> = [T] extends [any] ? T[] : never;
type Result = ToArrayNonDistributive<string | number>;
// Result: (string | number)[]Practical Use: Optional Parameters
interface BaseRouterConfig {
search?: string[];
}
type TupleToSearchParams<T extends string[]> = {
[K in T[number]]?: string;
};
// Only convert if search is defined and is a string array
type SearchParams<TConfig extends BaseRouterConfig, TRoute extends keyof TConfig> =
TConfig[TRoute]["search"] extends string[]
? TupleToSearchParams<TConfig[TRoute]["search"]>
: undefined;Using Conditionals in Function Arguments
const makeRouter = <TConfig extends Record<string, { search?: string[] }>>(
config: TConfig
) => {
return {
goTo: <TRoute extends keyof TConfig>(
route: TRoute,
// Only allow search params if route has search defined
search?: TConfig[TRoute]["search"] extends string[]
? { [K in TConfig[TRoute]["search"][number]]?: string }
: never
) => {
// Implementation
},
};
};
const router = makeRouter({
"/": {},
"/search": { search: ["query", "page"] },
});
router.goTo("/"); // No search param allowed
router.goTo("/search", { query: "test", page: "1" }); // Search params requiredFiltering with Conditionals
Use never to filter out types:
type ExtractStrings<T> = T extends string ? T : never;
type Mixed = "a" | "b" | 1 | 2 | true;
type OnlyStrings = ExtractStrings<Mixed>; // "a" | "b"This is how Extract and Exclude utilities work:
// Built-in utility implementations
type Extract<T, U> = T extends U ? T : never;
type Exclude<T, U> = T extends U ? never : T;Nested Conditionals
type DeepReadonly<T> = T extends Function
? T
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
interface User {
name: string;
address: {
city: string;
country: string;
};
greet: () => void;
}
type ReadonlyUser = DeepReadonly<User>;
// All properties including nested ones are readonly
// Functions remain unchangedChecking for Empty Types
// Check if array type is empty
type IsEmptyArray<T extends any[]> = T extends []
? true
: T extends [any, ...any[]]
? false
: boolean; // Unknown length arrays
type Test1 = IsEmptyArray<[]>; // true
type Test2 = IsEmptyArray<[1]>; // false
type Test3 = IsEmptyArray<string[]>; // boolean (unknown at compile time)Non-Empty Array Check
// Ensure array has at least one element
type NonEmptyArray<T extends any[]> = T extends [infer First, ...infer Rest]
? [First, ...Rest]
: never;
type Config = {
fields: ["name", "email"]; // Non-empty
};
// Use in conditional
type HasFields<T extends { fields?: string[] }> =
T["fields"] extends [string, ...string[]]
? true
: false;Common Patterns
Unwrap Promise Type
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type Test1 = UnwrapPromise<Promise<string>>; // string
type Test2 = UnwrapPromise<string>; // string (passthrough)Unwrap Array Type
type UnwrapArray<T> = T extends (infer U)[] ? U : T;
type Test1 = UnwrapArray<string[]>; // string
type Test2 = UnwrapArray<number>; // number (passthrough)Make All Properties Nullable
type Nullable<T> = T extends object
? { [K in keyof T]: T[K] | null }
: T | null;When to Use Conditional Types
- Type transformations: Different output types based on input
- Filtering unions: Extract or exclude certain types
- Optional type features: Enable features based on configuration
- Type guards: Return different types based on conditions
- Recursive types: Base cases in recursive type definitions
Common Pitfalls
Forgetting Distribution
type WrongIsArray<T> = T extends any[] ? true : false;
type Test = WrongIsArray<string | number[]>; // boolean (distributes!)
// If you want to check the entire union:
type CorrectIsArray<T> = [T] extends [any[]] ? true : false;
type Test2 = CorrectIsArray<string | number[]>; // falseOver-complicated Conditions
Sometimes union types or overloads are simpler:
// Over-complicated
type ProcessResult<T> = T extends string
? { type: "string"; value: string }
: T extends number
? { type: "number"; value: number }
: never;
// Simpler with discriminated union
type Result =
| { type: "string"; value: string }
| { type: "number"; value: number };Forgetting the False Branch
// Always provide a sensible false branch
type ExtractName<T> = T extends { name: infer N } ? N : never;
// Consider: what happens when T doesn't have name?
type Test = ExtractName<{ age: number }>; // neverDeep Type Inference
Overview
By default, TypeScript widens types when inferring objects and arrays. For advanced type-safe APIs, you often need to preserve literal types deeply within nested structures. This document covers techniques for achieving deep inference.
The Problem: Type Widening
const makeRouter = <TConfig>(config: TConfig) => {
return { config };
};
const router = makeRouter({
"/": {},
"/search": {
search: ["query", "page"],
},
});
// TConfig is inferred as:
// {
// "/": {};
// "/search": {
// search: string[]; // NOT ["query", "page"]!
// };
// }The literal tuple ["query", "page"] is widened to string[], losing type information.
Solution 1: User-Provided as const
Require users to add as const:
const router = makeRouter({
"/": {},
"/search": {
search: ["query", "page"],
},
} as const);
// Now TConfig preserves literals:
// {
// readonly "/": {};
// readonly "/search": {
// readonly search: readonly ["query", "page"];
// };
// }Drawbacks
- Users must remember to add
as const - Types become readonly (may require type adjustments)
- Easy to forget, leading to subtle bugs
Solution 2: F.Narrow from ts-toolbelt
The ts-toolbelt library provides F.Narrow for automatic deep narrowing:
import { F } from "ts-toolbelt";
const makeRouter = <TConfig extends BaseRouterConfig>(
config: F.Narrow<TConfig>,
) => {
return { config };
};
const router = makeRouter({
"/": {},
"/search": {
search: ["query", "page"],
},
});
// TConfig is now:
// {
// "/": {};
// "/search": {
// search: ["query", "page"]; // Literal tuple preserved!
// };
// }How F.Narrow Works
F.Narrow recursively narrows types to their literal forms:
- Strings become literal string types
- Numbers become literal number types
- Arrays become tuples
- Objects have their properties narrowed
Solution 3: Custom Narrow Type
If you can't use ts-toolbelt, implement a simpler version:
type Narrow<T> = T extends Function
? T
: T extends []
? []
: T extends readonly [infer First, ...infer Rest]
? [Narrow<First>, ...Narrow<Rest>]
: T extends object
? { [K in keyof T]: Narrow<T[K]> }
: T;
// Note: This is simplified and may not cover all edge casesPractical Example: Type-Safe Router
import { F } from "ts-toolbelt";
type BaseRouterConfig = Record<string, { search?: string[] }>;
type TupleToSearchParams<T extends string[]> = {
[K in T[number]]?: string;
};
const makeRouter = <TConfig extends BaseRouterConfig>(
config: F.Narrow<TConfig>,
) => {
return {
config,
goTo: <TRoute extends keyof TConfig>(
route: TRoute,
search?: TConfig[TRoute]["search"] extends string[]
? TupleToSearchParams<TConfig[TRoute]["search"]>
: never,
) => {
// Implementation
},
};
};
const router = makeRouter({
"/": {},
"/dashboard": {
search: ["page", "perPage", "sort"],
},
});
// Fully type-safe!
router.goTo("/dashboard", {
page: "1",
perPage: "10",
sort: "name", // Must be one of the defined search params
});
// Error: "invalid" is not a valid search param
router.goTo("/dashboard", { invalid: "value" });Solution 4: Const Type Parameter (TypeScript 5.0+)
TypeScript 5.0 introduced const type parameters:
const makeRouter = <const TConfig extends BaseRouterConfig>(
config: TConfig,
) => {
return { config };
};
// TConfig is automatically narrowed like as const
const router = makeRouter({
"/": {},
"/search": {
search: ["query", "page"],
},
});Benefits of const Type Parameters
- No external library needed
- Built into TypeScript
- Clean syntax
- Works with constraints
When Deep Inference Matters
Configuration Objects
const createTheme = <const TTheme extends Record<string, string>>(
theme: TTheme,
): TTheme => theme;
const theme = createTheme({
primary: "#0066cc",
secondary: "#666666",
});
// theme.primary is "#0066cc", not stringRoute Definitions
const routes = defineRoutes({
home: { path: "/" },
user: { path: "/users/:id" },
post: { path: "/posts/:postId" },
});
// Route names and paths are literal typesEvent Systems
const events = createEventMap({
click: (x: number, y: number) => {},
keydown: (key: string) => {},
});
// Event names are literal unions, handlers are properly typedComparison of Techniques
| Technique | Pros | Cons |
|---|---|---|
as const | No dependencies | Manual, readonly types |
F.Narrow | Automatic, flexible | External dependency |
| Custom Narrow | No dependencies, customizable | Complex, may miss edge cases |
const type param | Built-in, clean | TypeScript 5.0+ only |
Combining with Conditional Types
Deep inference enables powerful conditional type logic:
import { F } from "ts-toolbelt";
const makeApi = <const TConfig extends Record<string, { returns: string }>>(
config: TConfig,
) => {
return {
call: <TMethod extends keyof TConfig>(
method: TMethod,
): TConfig[TMethod]["returns"] => {
// Implementation
return "" as any;
},
};
};
const api = makeApi({
getUser: { returns: "User" },
getPost: { returns: "Post" },
});
const user = api.call("getUser"); // Type: "User"
const post = api.call("getPost"); // Type: "Post"Common Pitfalls
Forgetting Constraints
// Without constraint, F.Narrow has no base to work with
const bad = <TConfig>(config: F.Narrow<TConfig>) => config;
// With constraint, inference works properly
const good = <TConfig extends Record<string, unknown>>(
config: F.Narrow<TConfig>,
) => config;Readonly Arrays
With as const, arrays become readonly:
const config = {
values: [1, 2, 3],
} as const;
// config.values is readonly [1, 2, 3]
config.values.push(4); // Error: Property 'push' does not exist on type 'readonly [1, 2, 3]'Deep Nesting Performance
Very deeply nested types can slow down the compiler:
// May cause performance issues with extremely deep nesting
type DeepConfig = {
level1: {
level2: {
level3: {
// ... many more levels
};
};
};
};Best Practices
1. Use `const` type parameters when possible (TS 5.0+) 2. Fall back to F.Narrow for complex inference needs 3. Consider as const for simple, user-provided configs 4. Add proper constraints to guide inference 5. Test with complex examples to ensure inference works 6. Document the inference behavior for API consumers
Diagnosing TypeScript Errors
Overview
TypeScript errors can be cryptic, especially with complex generic types. This guide provides strategies for understanding and resolving type errors effectively.
General Strategies
1. Read Errors Bottom-Up
TypeScript writes errors top-down, but the actual cause is usually at the bottom:
Type '{ name: string; }' is not assignable to type 'User'.
Types of property 'email' are incompatible.
Type 'undefined' is not assignable to type 'string'.
^^^^^^
The actual issue!2. Hover for Type Information
Use IDE hover tooltips extensively:
const result = someFunction(arg);
// ^ Hover here to see the inferred type3. Use Go-to-Definition
Navigate to type definitions to understand what's expected:
document.querySelector("body");
// ^ Go-to-definition to see overloads4. Create Test Types
Extract parts of complex types to understand them:
// Complex expression
type Result = SomeComplexType<Input>[keyof Input][number];
// Break it down
type Step1 = SomeComplexType<Input>;
type Step2 = Step1[keyof Input];
type Step3 = Step2[number];Common Error Patterns
"Type 'X' is not assignable to type 'Y'"
The most common error. Check: 1. Are you missing properties? 2. Are property types incompatible? 3. Is there a literal vs widened type mismatch?
// Example: Literal type mismatch
const status = "active"; // Type: string (widened)
function setStatus(s: "active" | "inactive") {}
setStatus(status); // Error!
// Fix: Use as const
const status = "active" as const; // Type: "active"
setStatus(status); // OK"Property 'X' does not exist on type 'Y'"
The type doesn't have the expected property:
// Check 1: Is the type correct?
function process(data: unknown) {
data.name; // Error: 'name' doesn't exist on 'unknown'
}
// Fix: Add type guard
function process(data: unknown) {
if (typeof data === "object" && data !== null && "name" in data) {
data.name; // OK
}
}"Type 'X' cannot be used to index type 'Y'"
You're trying to access a property that might not exist:
function getValue<T>(obj: T, key: string) {
return obj[key]; // Error: string can't index T
}
// Fix: Constrain the key
function getValue<T, K extends keyof T>(obj: T, key: K) {
return obj[key]; // OK
}"Argument of type 'X' is not assignable to parameter of type 'Y'"
Function argument type mismatch:
// Often happens with narrower function signatures
const items = ["a", "b", "c"] as const;
items.includes(someString);
// Error: 'string' not assignable to '"a" | "b" | "c"'
// Fix: Cast appropriately
(items as readonly string[]).includes(someString);"Type 'X' is not generic"
Trying to pass type arguments to a non-generic type:
type NotGeneric = string;
type Attempt = NotGeneric<number>; // Error!
// Check if you need to add a generic parameter
type IsGeneric<T> = T;
type Works = IsGeneric<number>; // OKGeneric Constraint Errors
function process<T>(items: Parameters<T>) {}
// Error: 'T' does not satisfy constraint '(...args: any) => any'
// Fix: Add the constraint
function process<T extends (...args: any) => any>(items: Parameters<T>) {}Debugging Techniques
1. Simplify the Code
Remove complexity until the error is clear:
// Complex chain causing error
const result = complexFunction()
.map(transform)
.filter(predicate)
.reduce(accumulator);
// Simplify to isolate
const step1 = complexFunction();
// Check: Is step1 what you expect?
const step2 = step1.map(transform);
// Check: Is step2 what you expect?
// Continue until you find the issue2. Add Explicit Type Annotations
Force TypeScript to tell you what's wrong:
// Before: Error somewhere in here
const result = getData().process();
// After: Explicit annotations reveal issues
const data: ExpectedDataType = getData(); // Error if getData returns wrong type
const result: ExpectedResultType = data.process(); // Error if process returns wrong type3. Use // @ts-expect-error to Confirm Understanding
// If you think this should error:
// @ts-expect-error - string is not assignable to number
const x: number = "hello";
// If the @ts-expect-error is unused, TypeScript will tell you
// meaning the code is actually valid4. Check Source Definitions
For library types, check the actual definitions:
// In lib.dom.d.ts
interface Document {
querySelector<K extends keyof HTMLElementTagNameMap>(
selectors: K
): HTMLElementTagNameMap[K] | null;
querySelector(selectors: string): Element | null;
}Massive Error Messages
Strategy: Find the Core Issue
Long errors often have one core problem:
Type '{ fullName: string; id: string; firstName: string; lastName: string; age: number; }'
is not assignable to type
'{ fullName: string; id: string; firstName: string; lastName: string; age: number; agePlus10: number }'.
Property 'agePlus10' is missing in type
'{ fullName: string; id: string; firstName: string; lastName: string; age: number; }'
but required in type '{ fullName: string; agePlus10: number; }'.
^^^^^^^^^
The actual issue!Strategy: Use Type Aliases
Create type aliases to understand the comparison:
type Actual = typeof problematicValue;
type Expected = ExpectedType;
// Now hover these to compareInvestigating Library Types
Finding Type Definitions
1. Go-to-definition on imports 2. Check node_modules/@types/[library] 3. Check node_modules/[library]/dist/*.d.ts
Understanding Overloads
Look for (+N overload) in tooltips:
document.addEventListener("click", handler);
// ^ Shows (+1 overload)
// Go-to-definition to see all overloads
// The first matching overload is usedWhen Types Don't Match Reality
Sometimes library types are wrong or incomplete:
Solution 1: Type Assertion
// When you know better than TypeScript
const element = document.getElementById("root") as HTMLDivElement;Solution 2: Declaration Merging
// Extend existing types
declare module "some-library" {
interface SomeType {
missingProperty: string;
}
}Solution 3: Report the Issue
- Check if it's a known issue on GitHub
- File a bug report with reproduction
- Contribute a fix if possible
Prevention Strategies
1. Enable Strict Mode
{
"compilerOptions": {
"strict": true
}
}2. Avoid any
Every any is a potential type hole:
// Instead of
const data: any = fetchData();
// Use
const data: unknown = fetchData();
// Then narrow with type guards3. Use Proper Generics
// Instead of any in generics
function wrap<T = any>(value: T) {}
// Constrain appropriately
function wrap<T extends object>(value: T) {}4. Test Edge Cases
// Think about what could be passed
function process(input: string | string[]) {
// What if input is empty string?
// What if input is empty array?
// What if input has special characters?
}IDE Tips
1. Use TypeScript Version Selector: Match your project's version 2. Enable Inlay Hints: See inferred types inline 3. Use Quick Fix: Often suggests the correct solution 4. Check Problems Panel: See all errors at once 5. Use Rename Symbol: Safely rename types across files
Function Overloads
Overview
Function overloads allow you to define multiple function signatures for a single function implementation. TypeScript selects the appropriate signature based on the arguments provided.
Basic Syntax
// Overload signatures (what callers see)
function greet(name: string): string;
function greet(firstName: string, lastName: string): string;
// Implementation signature (must be compatible with all overloads)
function greet(nameOrFirst: string, lastName?: string): string {
if (lastName) {
return `Hello, ${nameOrFirst} ${lastName}!`;
}
return `Hello, ${nameOrFirst}!`;
}
// Usage - TypeScript picks the right overload
greet("Alice"); // Uses first overload
greet("Alice", "Smith"); // Uses second overloadOverload Resolution: Top to Bottom
TypeScript tries overloads in order from top to bottom, using the first that matches:
// Order matters! More specific overloads should come first
function processValue(value: string): string;
function processValue(value: number): number;
function processValue(value: string | number): string | number {
if (typeof value === "string") {
return value.toUpperCase();
}
return value * 2;
}
const str = processValue("hello"); // Type: string
const num = processValue(42); // Type: numberReal-World Example: DOM querySelector
The DOM's querySelector uses overloads for element type inference:
// Simplified version of what lib.dom.d.ts defines
interface Document {
// Specific overload for known HTML elements
querySelector<K extends keyof HTMLElementTagNameMap>(
selectors: K
): HTMLElementTagNameMap[K] | null;
// Fallback for custom selectors
querySelector(selectors: string): Element | null;
}
const body = document.querySelector("body"); // Type: HTMLBodyElement | null
const custom = document.querySelector(".my-class"); // Type: Element | nullPattern: Wrapping Functions with Overloads
When wrapping a function, mirror its overloads to preserve type inference:
// Problem: Simple wrapper loses overload behavior
export function nonNullQuerySelector(tag: string) {
const element = document.querySelector(tag);
if (!element) {
throw new Error(`Element not found: ${tag}`);
}
return element;
}
const body = nonNullQuerySelector("body"); // Type: Element (lost HTMLBodyElement!)
// Solution: Add overload that mirrors querySelector
export function nonNullQuerySelector<K extends keyof HTMLElementTagNameMap>(
tag: K
): HTMLElementTagNameMap[K];
export function nonNullQuerySelector(tag: string): Element;
export function nonNullQuerySelector(tag: string): Element {
const element = document.querySelector(tag);
if (!element) {
throw new Error(`Element not found: ${tag}`);
}
return element;
}
const body = nonNullQuerySelector("body"); // Type: HTMLBodyElement
const custom = nonNullQuerySelector(".custom"); // Type: ElementMethod Overloads in Classes
class Calculator {
add(a: number, b: number): number;
add(a: string, b: string): string;
add(a: number | string, b: number | string): number | string {
if (typeof a === "number" && typeof b === "number") {
return a + b;
}
return String(a) + String(b);
}
}
const calc = new Calculator();
const sum = calc.add(1, 2); // Type: number
const concat = calc.add("hello", "world"); // Type: stringOverloads in Object Types
interface StringOrNumberFunc {
(value: string): string;
(value: number): number;
}
const process: StringOrNumberFunc = (value: string | number) => {
if (typeof value === "string") {
return value.toUpperCase();
}
return value * 2;
};Event Handler Pattern
A common pattern for event systems:
interface EventMap {
click: MouseEvent;
keydown: KeyboardEvent;
submit: SubmitEvent;
}
interface EventEmitter {
// Specific overload for known events
on<K extends keyof EventMap>(
event: K,
handler: (e: EventMap[K]) => void
): void;
// Fallback for custom events
on(event: string, handler: (e: Event) => void): void;
}
const emitter: EventEmitter = {
on(event: string, handler: (e: any) => void) {
// Implementation
},
};
// Handler type is correctly inferred
emitter.on("click", (e) => {
console.log(e.clientX); // e is MouseEvent
});
emitter.on("keydown", (e) => {
console.log(e.key); // e is KeyboardEvent
});
emitter.on("custom", (e) => {
// e is Event (fallback)
});Overloads vs Union Types
Sometimes a union type is simpler than overloads:
// Overloads - when return type depends on input type
function parse(input: string): object;
function parse(input: object): string;
function parse(input: string | object): string | object {
if (typeof input === "string") {
return JSON.parse(input);
}
return JSON.stringify(input);
}
// Union - when return type is always the same
function process(input: string | number): string {
return String(input);
}Overloads with Optional Parameters
function createElement(tag: "input"): HTMLInputElement;
function createElement(tag: "button", text?: string): HTMLButtonElement;
function createElement(tag: string, text?: string): HTMLElement;
function createElement(tag: string, text?: string): HTMLElement {
const element = document.createElement(tag);
if (text) {
element.textContent = text;
}
return element;
}Common Pitfalls
Implementation Signature Visibility
The implementation signature is NOT visible to callers:
function example(a: string): string;
function example(a: number): number;
function example(a: string | number): string | number {
return typeof a === "string" ? a.toUpperCase() : a * 2;
}
// Error: No overload matches this call
example(true); // Even though implementation accepts anyWrong Overload Order
Put specific overloads before general ones:
// BAD - general overload catches everything
function bad(x: any): any;
function bad(x: string): string; // Never reached!
function bad(x: any): any {
return x;
}
// GOOD - specific overloads first
function good(x: string): string;
function good(x: any): any;
function good(x: any): any {
return x;
}Implementation Must Be Compatible
The implementation signature must handle all overload cases:
function process(x: string): string;
function process(x: number): number;
// Error: Implementation signature must be compatible
function process(x: string): string {
return x.toUpperCase();
}
// Correct
function process(x: string | number): string | number {
if (typeof x === "string") {
return x.toUpperCase();
}
return x * 2;
}When to Use Overloads
- Return type depends on input type: Different inputs produce different output types
- Wrapping external APIs: Mirror the overloads of the wrapped function
- Event systems: Map event names to their event types
- Factory functions: Different configurations produce different types
- API compatibility: Provide multiple call signatures for the same operation
When NOT to Use Overloads
- Simple unions: If return type doesn't depend on input type, use unions
- Optional parameters: Often simpler than multiple overloads
- Generics: Sometimes a single generic signature is clearer
Generics Fundamentals
Overview
Generics allow you to create reusable components that work with multiple types while maintaining type safety. They're essential for building flexible, type-safe APIs.
Basic Generic Functions
// Without generics - loses type information
function identity(value: any): any {
return value;
}
// With generics - preserves type
function identity<T>(value: T): T {
return value;
}
const num = identity(42); // Type: number (inferred)
const str = identity("hello"); // Type: string (inferred)
const explicit = identity<boolean>(true); // Type: boolean (explicit)Inference Dependencies
When the type of one parameter depends on another, use generics:
// The return type depends on what keys exist in the config
const createComponent = <TConfig extends Record<string, string>>(
config: TConfig,
) => {
return (variant: keyof TConfig, ...otherClasses: string[]): string => {
return config[variant] + " " + otherClasses.join(" ");
};
};
// TConfig is inferred as { primary: string; secondary: string }
const getButtonClasses = createComponent({
primary: "bg-blue-300",
secondary: "bg-green-300",
});
// variant must be "primary" | "secondary"
getButtonClasses("primary", "px-4"); // OK
getButtonClasses("tertiary", "px-4"); // Error: "tertiary" not in keysGeneric Constraints with extends
Constrain generics to ensure they have required properties:
// Unconstrained - TFunc could be anything
type WrapFunction<TFunc> = (...args: any[]) => any;
// Constrained - TFunc must be a function
type WrapFunction<TFunc extends (...args: any) => any> = (
...args: Parameters<TFunc>
) => ReturnType<TFunc>;Why Constraints Matter
// Without constraint
function getLength<T>(item: T): number {
return item.length; // Error: Property 'length' does not exist on type 'T'
}
// With constraint
function getLength<T extends { length: number }>(item: T): number {
return item.length; // OK - we know T has length
}
getLength("hello"); // 5
getLength([1, 2, 3]); // 3
getLength({ length: 10 }); // 10
getLength(42); // Error: number doesn't have lengthDefault Generic Parameters
Provide defaults for optional type parameters:
type WrapFunction<
TFunc extends (...args: any) => any,
TAdditional = {} // Default to empty object
> = (
...args: Parameters<TFunc>
) => Promise<Awaited<ReturnType<TFunc>> & TAdditional>;
// Can use without TAdditional
type BasicWrapper = WrapFunction<typeof fetchUser>;
// Or with TAdditional
type ExtendedWrapper = WrapFunction<typeof fetchUser, { meta: string }>;Generic Slot Inference
TypeScript infers generic types from usage:
// Generic is inferred from the argument passed
const createComponent = <TConfig>(config: TConfig) => {
return config;
};
// TConfig is inferred as { primary: string; secondary: string }
const component = createComponent({
primary: "bg-blue-300",
secondary: "bg-green-300",
});When Inference Doesn't Work
If you don't USE the generic in arguments, it defaults to unknown:
// BAD - TConfig isn't used in arguments, defaults to unknown
const createComponent = <TConfig>(config: Record<string, string>) => {
// TConfig is unknown here
};
// GOOD - TConfig IS the argument type
const createComponent = <TConfig extends Record<string, string>>(
config: TConfig,
) => {
// TConfig is inferred from what's passed
};Multiple Generic Parameters
Use multiple parameters for related but distinct types:
function map<TInput, TOutput>(
items: TInput[],
transform: (item: TInput) => TOutput
): TOutput[] {
return items.map(transform);
}
// Both TInput and TOutput are inferred
const numbers = map(["1", "2", "3"], (s) => parseInt(s));
// TInput: string, TOutput: number, Result: number[]Pattern: keyof with Generics
Combine keyof with generics for type-safe property access:
function getProperty<TObj, TKey extends keyof TObj>(
obj: TObj,
key: TKey
): TObj[TKey] {
return obj[key];
}
const user = { name: "Alice", age: 30 };
const name = getProperty(user, "name"); // Type: string
const age = getProperty(user, "age"); // Type: number
const invalid = getProperty(user, "email"); // Error: "email" not in keyofGenerics in Classes
class Container<T> {
private value: T;
constructor(value: T) {
this.value = value;
}
getValue(): T {
return this.value;
}
map<U>(transform: (value: T) => U): Container<U> {
return new Container(transform(this.value));
}
}
const numContainer = new Container(42);
const strContainer = numContainer.map((n) => n.toString());
// strContainer is Container<string>Complete Example: Component Factory
// A factory that creates type-safe component class generators
export const createComponent = <TConfig extends Record<string, string>>(
config: TConfig,
) => {
// Return a function that requires valid variant keys
return (variant: keyof TConfig, ...otherClasses: string[]): string => {
return config[variant] + " " + otherClasses.join(" ");
};
};
// Usage
const getButtonClasses = createComponent({
primary: "bg-blue-500 text-white",
secondary: "bg-gray-200 text-gray-800",
danger: "bg-red-500 text-white",
});
// Type-safe: variant must be "primary" | "secondary" | "danger"
const classes = getButtonClasses("primary", "px-4", "py-2");
// Result: "bg-blue-500 text-white px-4 py-2"
// Type error on invalid variant
getButtonClasses("invalid"); // Error!When to Use Generics
- Type preservation: When you need to preserve type information through a function
- Inference dependencies: When one type depends on another
- Reusable components: When building APIs that work with multiple types
- Constraints: When you need to ensure types have certain properties
- Factory functions: When creating functions that return typed results
Common Pitfalls
Unnecessary Generics
// BAD - generic provides no value
function greet<T extends string>(name: T): string {
return `Hello, ${name}`;
}
// GOOD - just use string
function greet(name: string): string {
return `Hello, ${name}`;
}Over-constraining
// BAD - overly specific constraint
function process<T extends { id: string; name: string; email: string }>(
obj: T
): void {}
// GOOD - only require what you actually use
function process<T extends { id: string }>(obj: T): void {}Forgetting to Constrain
// BAD - accessing property that might not exist
function getName<T>(obj: T): string {
return obj.name; // Error: Property 'name' does not exist
}
// GOOD - constrain to types that have name
function getName<T extends { name: string }>(obj: T): string {
return obj.name;
}The infer Keyword
Overview
The infer keyword allows you to extract and capture type information within conditional types. It's like pattern matching for types - you define a pattern and capture parts of it.
Basic Syntax
type ExtractType<T> = T extends SomePattern<infer U> ? U : never;
// ^^^^^^^^
// Captures this part into USimple Examples
Extract Array Element Type
type ArrayElement<T> = T extends (infer U)[] ? U : never;
type Test1 = ArrayElement<string[]>; // string
type Test2 = ArrayElement<number[]>; // number
type Test3 = ArrayElement<(string | number)[]>; // string | number
type Test4 = ArrayElement<string>; // never (not an array)Extract Promise Value
type PromiseValue<T> = T extends Promise<infer U> ? U : never;
type Test1 = PromiseValue<Promise<string>>; // string
type Test2 = PromiseValue<Promise<number>>; // number
type Test3 = PromiseValue<string>; // neverExtract Object Property Type
type GetData<T> = T extends { data: infer TData } ? TData : never;
type Test1 = GetData<{ data: string }>; // string
type Test2 = GetData<{ data: number[] }>; // number[]
type Test3 = GetData<{ other: string }>; // neverTemplate Literal Type Extraction
infer works powerfully with template literal types:
// Remove "maps:" prefix from string
type RemoveMaps<T> = T extends `maps:${infer Rest}` ? Rest : T;
type Test1 = RemoveMaps<"maps:longitude">; // "longitude"
type Test2 = RemoveMaps<"maps:latitude">; // "latitude"
type Test3 = RemoveMaps<"other">; // "other" (no match, returns T)Multiple Captures in Template Literals
// Parse route parameters
type ParseRoute<T> = T extends `${infer Start}:${infer Param}/${infer Rest}`
? { start: Start; param: Param; rest: ParseRoute<Rest> }
: T extends `${infer Start}:${infer Param}`
? { start: Start; param: Param }
: T;
type Route = ParseRoute<"/users/:id/posts/:postId">;
// Nested structure with extracted paramsExtract Before/After Patterns
// Get everything before ":"
type Before<T> = T extends `${infer Prefix}:${string}` ? Prefix : T;
// Get everything after ":"
type After<T> = T extends `${string}:${infer Suffix}` ? Suffix : T;
type Test1 = Before<"prefix:suffix">; // "prefix"
type Test2 = After<"prefix:suffix">; // "suffix"Function Type Extraction
Extract Return Type
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Test = MyReturnType<() => string>; // stringExtract Parameter Types
type MyParameters<T> = T extends (...args: infer P) => any ? P : never;
type Test = MyParameters<(a: string, b: number) => void>;
// [a: string, b: number]Extract First Parameter
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any
? F
: never;
type Test = FirstArg<(name: string, age: number) => void>; // stringExtract Constructor Parameters
type ConstructorParams<T> = T extends new (...args: infer P) => any ? P : never;
class User {
constructor(
public name: string,
public age: number,
) {}
}
type UserParams = ConstructorParams<typeof User>; // [string, number]Multiple infer in One Condition
You can use multiple infer captures:
// Extract key-value from "key=value" string
type ParseKeyValue<T> = T extends `${infer Key}=${infer Value}`
? { key: Key; value: Value }
: never;
type Test = ParseKeyValue<"name=John">;
// { key: "name"; value: "John" }Infer with Constraints
You can add constraints to inferred types:
// Only infer if it's a string
type ExtractString<T> = T extends { value: infer V extends string } ? V : never;
type Test1 = ExtractString<{ value: "hello" }>; // "hello"
type Test2 = ExtractString<{ value: 123 }>; // neverRecursive Type Extraction
// Deeply unwrap nested promises
type DeepAwaited<T> = T extends Promise<infer U> ? DeepAwaited<U> : T;
type Test = DeepAwaited<Promise<Promise<Promise<string>>>>; // stringPractical Examples
Type-Safe Event Emitter
type EventHandler<T> = T extends (event: infer E) => void ? E : never;
interface Events {
click: (event: MouseEvent) => void;
keydown: (event: KeyboardEvent) => void;
}
type ClickEvent = EventHandler<Events["click"]>; // MouseEventExtract Route Parameters
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/:userId/posts/:postId">;
// "userId" | "postId"Object Key Transformation
// Remove "maps:" prefix from all object keys
type RemoveMaps<T> = T extends `maps:${infer Rest}` ? Rest : T;
type RemoveMapsPrefixFromObj<T> = {
[K in keyof T as RemoveMaps<K>]: T[K];
};
interface ApiData {
"maps:longitude": string;
"maps:latitude": string;
city: string;
}
type Cleaned = RemoveMapsPrefixFromObj<ApiData>;
// { longitude: string; latitude: string; city: string }Extract Generic Parameters
type ExtractGeneric<T> =
T extends Array<infer U>
? U
: T extends Map<infer K, infer V>
? { key: K; value: V }
: T extends Set<infer U>
? U
: never;
type Test1 = ExtractGeneric<Array<string>>; // string
type Test2 = ExtractGeneric<Map<string, number>>; // { key: string; value: number }
type Test3 = ExtractGeneric<Set<boolean>>; // booleanCommon Pitfalls
Infer Position Matters
// Captures the FIRST matching position
type First<T> = T extends [infer F, ...any[]] ? F : never;
type Last<T> = T extends [...any[], infer L] ? L : never;
type TestFirst = First<[1, 2, 3]>; // 1
type TestLast = Last<[1, 2, 3]>; // 3Greedy Template Literal Matching
// Greedy: captures as much as possible
type GetPath<T> = T extends `${infer Path}.json` ? Path : never;
type Test = GetPath<"folder/file.name.json">;
// "folder/file.name" (not "folder/file")Union Distribution with Infer
type ExtractArray<T> = T extends (infer U)[] ? U : never;
// Distributes over union
type Test = ExtractArray<string[] | number[]>;
// string | number (not (string | number)[])When to Use infer
- Type extraction: Pull types out of complex structures
- String parsing: Extract parts from template literal types
- Function analysis: Get parameter/return types
- Pattern matching: Match and capture type patterns
- Recursive types: Extract types in recursive structures
Best Practices
1. Provide fallback types: Always handle the false branch 2. Be specific with patterns: More specific patterns = better inference 3. Consider distribution: Remember that union types distribute 4. Name captures meaningfully: Use descriptive names like TData, TKey, TValue
Mapped Types
Overview
Mapped types allow you to create new types by transforming each property of an existing type. They iterate over keys and apply transformations to create new type structures.
Basic Syntax
type MappedType<T> = {
[K in keyof T]: TransformedType;
};Simple Examples
Make All Properties Optional
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
interface User {
id: string;
name: string;
email: string;
}
type PartialUser = MyPartial<User>;
// { id?: string; name?: string; email?: string }Make All Properties Required
type MyRequired<T> = {
[K in keyof T]-?: T[K]; // -? removes optional modifier
};Make All Properties Readonly
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};Make All Properties Mutable
type Mutable<T> = {
-readonly [K in keyof T]: T[K]; // -readonly removes readonly modifier
};Preserving Original Keys
When you just iterate over keyof T, you preserve the original keys:
type Preserve<T> = {
[K in keyof T]: T[K]; // Same type, just recreated
};Key Remapping with as
Transform keys while mapping using the as clause:
type RemapKeys<T> = {
[K in keyof T as NewKeyType]: T[K];
};Add Prefix to Keys
type Prefixed<T, P extends string> = {
[K in keyof T as K extends string ? `${P}${K}` : K]: T[K];
};
interface User {
name: string;
age: number;
}
type PrefixedUser = Prefixed<User, "user_">;
// { user_name: string; user_age: number }Remove Keys by Remapping to never
type RemoveFields<T, K extends keyof T> = {
[P in keyof T as P extends K ? never : P]: T[P];
};
type UserWithoutEmail = RemoveFields<User, "email">;
// { id: string; name: string }Transform Keys
type RemoveMapsPrefixFromObj<T> = {
[K in keyof T as RemoveMaps<K>]: T[K];
};
type RemoveMaps<T> = T extends `maps:${infer Rest}` ? Rest : T;
interface ApiData {
"maps:longitude": string;
"maps:latitude": string;
}
type CleanData = RemoveMapsPrefixFromObj<ApiData>;
// { longitude: string; latitude: string }Filtering Keys
Use conditional types in the as clause to filter:
// Only keep string properties
type OnlyStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
interface Mixed {
name: string;
age: number;
email: string;
active: boolean;
}
type StringProps = OnlyStrings<Mixed>;
// { name: string; email: string }Keep Only Required Properties
type RequiredKeys<T> = {
[K in keyof T]-?: undefined extends T[K] ? never : K;
}[keyof T];
type OnlyRequired<T> = Pick<T, RequiredKeys<T>>;Keep Only Optional Properties
type OptionalKeys<T> = {
[K in keyof T]-?: undefined extends T[K] ? K : never;
}[keyof T];
type OnlyOptional<T> = Pick<T, OptionalKeys<T>>;Transforming Property Types
Wrap All Properties in Promise
type Promisify<T> = {
[K in keyof T]: Promise<T[K]>;
};
interface SyncApi {
getUser(): User;
getPost(): Post;
}
type AsyncApi = Promisify<SyncApi>;
// { getUser: Promise<() => User>; getPost: Promise<() => Post> }Make All Properties Arrays
type Arrayify<T> = {
[K in keyof T]: T[K][];
};
interface Single {
name: string;
count: number;
}
type Multiple = Arrayify<Single>;
// { name: string[]; count: number[] }Nullable Properties
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};Deep Mapped Types
Apply transformations recursively:
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
interface Nested {
user: {
profile: {
name: string;
};
};
}
type ReadonlyNested = DeepReadonly<Nested>;
// All levels are readonlyDeep Partial
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};Practical Examples
Getters and Setters
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type Setters<T> = {
[K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }
type PersonSetters = Setters<Person>;
// { setName: (value: string) => void; setAge: (value: number) => void }Event Handlers
type EventHandlers<T> = {
[K in keyof T as `on${Capitalize<string & K>}Change`]: (
newValue: T[K],
oldValue: T[K],
) => void;
};
interface State {
count: number;
name: string;
}
type StateHandlers = EventHandlers<State>;
// {
// onCountChange: (newValue: number, oldValue: number) => void;
// onNameChange: (newValue: string, oldValue: string) => void;
// }Form Validation Errors
type ValidationErrors<T> = {
[K in keyof T]?: string[];
};
interface RegistrationForm {
email: string;
password: string;
confirmPassword: string;
}
type RegistrationErrors = ValidationErrors<RegistrationForm>;
// { email?: string[]; password?: string[]; confirmPassword?: string[] }Combining Mapped Types
Pick with Transformation
type PickAndTransform<T, K extends keyof T> = {
[P in K]: T[P] extends Function ? T[P] : Readonly<T[P]>;
};Merge Two Types
type Merge<A, B> = {
[K in keyof A | keyof B]: K extends keyof B
? B[K]
: K extends keyof A
? A[K]
: never;
};Index Signatures in Mapped Types
// Create index signature from union
type FromUnion<K extends string, V> = {
[P in K]: V;
};
type Dict = FromUnion<"a" | "b" | "c", number>;
// { a: number; b: number; c: number }Common Pitfalls
Forgetting String Key Check
Template literals require string keys:
// Error: Type 'K' is not assignable to type 'string'
type Wrong<T> = {
[K in keyof T as `prefix_${K}`]: T[K];
};
// Correct: Check K extends string
type Correct<T> = {
[K in keyof T as K extends string ? `prefix_${K}` : never]: T[K];
};Losing Modifiers
Remapping can lose optional/readonly modifiers:
// Original optional modifier lost
type Transform<T> = {
[K in keyof T as `new_${string & K}`]: T[K];
};
// Preserve optional with conditional
type TransformPreserve<T> = {
[K in keyof T as `new_${string & K}`]+?: T[K];
};Infinite Recursion
Deep mapped types can cause issues:
// Potential infinite recursion with circular types
type DeepReadonly<T> = {
readonly [K in keyof T]: DeepReadonly<T[K]>;
};
// Add base case for primitives
type DeepReadonlySafe<T> = T extends object
? { readonly [K in keyof T]: DeepReadonlySafe<T[K]> }
: T;When to Use Mapped Types
- Type transformations: Change modifiers (optional, readonly)
- Key renaming: Add prefixes, suffixes, or transform key names
- Property filtering: Remove or keep certain properties
- Bulk operations: Apply same transformation to all properties
- Type utilities: Build reusable type transformers
Opaque Types (Brand Types)
Overview
Opaque types (also called brand types or nominal types) create distinct types from primitive types. They prevent mixing up values that have the same underlying type but different semantic meanings.
The Problem
TypeScript uses structural typing, so these are interchangeable:
type UserId = string;
type PostId = string;
function getUser(id: UserId): User {
/* ... */
}
function getPost(id: PostId): Post {
/* ... */
}
const userId: UserId = "user-123";
const postId: PostId = "post-456";
// BUG: Wrong ID type, but TypeScript allows it!
getUser(postId); // No error - both are just stringsCreating Opaque Types
Add a phantom property to create nominal distinction:
type Opaque<TValue, TBrand> = TValue & { __brand: TBrand };
type UserId = Opaque<string, "UserId">;
type PostId = Opaque<string, "PostId">;
type ValidEmail = Opaque<string, "ValidEmail">;
type ValidAge = Opaque<number, "ValidAge">;Now these types are incompatible:
function getUser(id: UserId): User {
/* ... */
}
function getPost(id: PostId): Post {
/* ... */
}
const userId = "user-123" as UserId;
const postId = "post-456" as PostId;
getUser(userId); // OK
getUser(postId); // Error: Type 'PostId' is not assignable to type 'UserId'Type Predicates for Validation
Use type predicates to validate and narrow types:
type ValidEmail = Opaque<string, "ValidEmail">;
// Type predicate: "email is ValidEmail" narrows the type
const isValidEmail = (email: string): email is ValidEmail => {
return email.includes("@") && email.includes(".");
};
// Usage with type narrowing
function processEmail(email: string): void {
if (!isValidEmail(email)) {
throw new Error("Invalid email");
}
// email is now ValidEmail
sendEmail(email); // Type-safe!
}
function sendEmail(email: ValidEmail): void {
// We know the email has been validated
}Assertion Functions
Assertion functions throw on invalid input and narrow the type:
type ValidEmail = Opaque<string, "ValidEmail">;
// Assertion function - must be declared with function, not arrow
function assertValidEmail(email: string): asserts email is ValidEmail {
if (!email.includes("@") || !email.includes(".")) {
throw new Error("Invalid email format");
}
}
// Usage
async function createUser(data: { email: string }): Promise<User> {
assertValidEmail(data.email);
// data.email is now ValidEmail
return await saveUser({
email: data.email, // Type-safe!
});
}Important: Assertion Function Syntax
Assertion functions MUST be declared using the function keyword, not arrow functions:
// WRONG - arrow functions don't work with asserts
const assertValidEmail = (email: string): asserts email is ValidEmail => {
// Error: Assertions require every name in the call target to be
// declared with an explicit type annotation.
};
// CORRECT - use function declaration
function assertValidEmail(email: string): asserts email is ValidEmail {
if (!email.includes("@")) {
throw new Error("Invalid email");
}
}Comparison: Type Predicates vs Assertion Functions
| Aspect | Type Predicate | Assertion Function |
|---|---|---|
| Return | boolean | void (throws on failure) |
| Usage | In if statements | Standalone call |
| Error handling | Caller handles | Function throws |
| Syntax | Arrow or function | Must be function |
// Type predicate - returns boolean, caller handles failure
if (!isValidEmail(email)) {
return { error: "Invalid email" };
}
sendEmail(email);
// Assertion function - throws, cleaner happy path
assertValidEmail(email);
sendEmail(email);Complete Example: User Registration
type Opaque<TValue, TBrand> = TValue & { __brand: TBrand };
type ValidEmail = Opaque<string, "ValidEmail">;
type ValidPassword = Opaque<string, "ValidPassword">;
type UserId = Opaque<string, "UserId">;
// Validation functions
function assertValidEmail(email: string): asserts email is ValidEmail {
if (!email.includes("@") || email.length < 5) {
throw new Error("Invalid email format");
}
}
function assertValidPassword(
password: string,
): asserts password is ValidPassword {
if (password.length < 8) {
throw new Error("Password must be at least 8 characters");
}
}
// Database functions require validated types
async function createUser(data: {
email: ValidEmail;
password: ValidPassword;
}): Promise<{ id: UserId }> {
// We know email and password are validated
return { id: crypto.randomUUID() as UserId };
}
// API handler
async function handleRegistration(input: { email: string; password: string }) {
// Must validate before calling createUser
assertValidEmail(input.email);
assertValidPassword(input.password);
// Now we can safely call createUser
const user = await createUser({
email: input.email,
password: input.password,
});
return user;
}Pattern: Factory Functions for Opaque Types
For cases where you want to validate at creation time:
type UserId = Opaque<string, "UserId">;
// Factory function that validates and creates
function createUserId(id: string): UserId {
if (!id.startsWith("user_")) {
throw new Error("Invalid user ID format");
}
return id as UserId;
}
// Or with a type predicate for conditional creation
function parseUserId(id: string): UserId | null {
if (!id.startsWith("user_")) {
return null;
}
return id as UserId;
}When to Use Opaque Types
- IDs: UserId, PostId, OrderId - prevent mixing different entity IDs
- Validated strings: Email, URL, Phone - ensure validation has occurred
- Validated numbers: Age, Price, Quantity - ensure range validation
- Security-sensitive: HashedPassword, APIKey - prevent accidental exposure
Common Pitfalls
Direct Assignment Bypasses Type Safety
const email: ValidEmail = "invalid"; // Error at compile time
// But casting bypasses safety
const email = "invalid" as ValidEmail; // No error, but potentially wrong!Forgetting to Validate
function processUser(userId: UserId): void {
// ...
}
// BAD - casting without validation
processUser(request.body.id as UserId);
// GOOD - validate first
function assertUserId(id: string): asserts id is UserId {
if (!id.startsWith("user_")) throw new Error("Invalid user ID");
}
assertUserId(request.body.id);
processUser(request.body.id);Alternative: Unique Symbol Brand
A more robust branding approach using unique symbols:
declare const brand: unique symbol;
type Brand<T, TBrand> = T & { [brand]: TBrand };
type UserId = Brand<string, "UserId">;
type PostId = Brand<string, "PostId">;
// This is slightly more type-safe as __brand could theoretically
// be a real property, but unique symbol cannotTemplate Literal Types
Overview
Template literal types allow you to manipulate string types using the same syntax as JavaScript template literals. Combined with infer, they enable powerful string parsing and transformation at the type level.
Basic Syntax
type Greeting = `Hello, ${string}`;
const valid: Greeting = "Hello, World"; // OK
const invalid: Greeting = "Hi, World"; // Error: doesn't match patternString Literal Unions
Template literals distribute over unions:
type Size = "small" | "medium" | "large";
type Color = "red" | "blue" | "green";
type SizedColor = `${Size}-${Color}`;
// "small-red" | "small-blue" | "small-green" |
// "medium-red" | "medium-blue" | "medium-green" |
// "large-red" | "large-blue" | "large-green"Pattern Matching with infer
Extract parts of string types:
// Remove "maps:" prefix
type RemoveMaps<T> = T extends `maps:${infer Rest}` ? Rest : T;
type Test1 = RemoveMaps<"maps:longitude">; // "longitude"
type Test2 = RemoveMaps<"maps:latitude">; // "latitude"
type Test3 = RemoveMaps<"other">; // "other"Remove Suffix
type RemovePostSuffix<T> = T extends `${infer Prefix}:post` ? Prefix : T;
type Test = RemovePostSuffix<"attribute:post">; // "attribute"Split on Delimiter
type Split<
S extends string,
D extends string,
> = S extends `${infer Head}${D}${infer Tail}`
? [Head, ...Split<Tail, D>]
: S extends ""
? []
: [S];
type Parts = Split<"a-b-c", "-">; // ["a", "b", "c"]Built-in String Manipulation Types
TypeScript provides utility types for case conversion:
type Upper = Uppercase<"hello">; // "HELLO"
type Lower = Lowercase<"HELLO">; // "hello"
type Cap = Capitalize<"hello">; // "Hello"
type Uncap = Uncapitalize<"Hello">; // "hello"Practical Examples
CSS Property to Camel Case
type CamelCase<S extends string> =
S extends `${infer P1}-${infer P2}${infer P3}`
? `${Lowercase<P1>}${Uppercase<P2>}${CamelCase<P3>}`
: Lowercase<S>;
type Test = CamelCase<"background-color">; // "backgroundColor"
type Test2 = CamelCase<"border-top-width">; // "borderTopWidth"Event Name Generation
type EventName<T extends string> = `on${Capitalize<T>}`;
type MouseEvents = "click" | "mousedown" | "mouseup";
type MouseHandlers = EventName<MouseEvents>;
// "onClick" | "onMousedown" | "onMouseup"Getter/Setter Names
type Getter<T extends string> = `get${Capitalize<T>}`;
type Setter<T extends string> = `set${Capitalize<T>}`;
type PropName = "name" | "age";
type Getters = Getter<PropName>; // "getName" | "getAge"
type Setters = Setter<PropName>; // "setName" | "setAge"Object Key Transformation
Add Prefix to Keys
type AddPrefix<T, P extends string> = {
[K in keyof T as K extends string ? `${P}${K}` : K]: T[K];
};
interface User {
name: string;
age: number;
}
type PrefixedUser = AddPrefix<User, "user_">;
// { user_name: string; user_age: number }Add Suffix to Keys
type AddSuffix<T, S extends string> = {
[K in keyof T as K extends string ? `${K}${S}` : never]: T[K];
};
interface Data {
a: number;
b: number;
}
type NewData = AddSuffix<Data, "_new">;
// { a_new: number; b_new: number }Transform Keys from snake_case to camelCase
type SnakeToCamel<S extends string> =
S extends `${infer P1}_${infer P2}${infer P3}`
? `${Lowercase<P1>}${Uppercase<P2>}${SnakeToCamel<P3>}`
: S;
type CamelizeKeys<T> = {
[K in keyof T as K extends string ? SnakeToCamel<K> : K]: T[K];
};
interface ApiResponse {
user_id: string;
first_name: string;
last_name: string;
}
type CamelResponse = CamelizeKeys<ApiResponse>;
// { userId: string; firstName: string; lastName: string }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 typed params object
type RouteParams<T extends string> = {
[K in ExtractRouteParams<T>]: string;
};
type UserPostParams = RouteParams<"/users/:userId/posts/:postId">;
// { userId: string; postId: string }Validation Patterns
Email Pattern (Simplified)
type ValidEmail = `${string}@${string}.${string}`;
function validateEmail<T extends string>(
email: T extends ValidEmail ? T : never,
): T {
return email;
}
validateEmail("user@example.com"); // OK
validateEmail("invalid"); // ErrorURL Pattern
type Protocol = "http" | "https";
type ValidUrl = `${Protocol}://${string}`;
function fetchUrl(url: ValidUrl): Promise<Response> {
return fetch(url);
}
fetchUrl("https://api.example.com"); // OK
fetchUrl("ftp://files.example.com"); // ErrorComplex Parsing
Parse Query String Type
type ParseQueryString<T extends string> =
T extends `${infer Key}=${infer Value}&${infer Rest}`
? { [K in Key]: Value } & ParseQueryString<Rest>
: T extends `${infer Key}=${infer Value}`
? { [K in Key]: Value }
: {};
type QueryParams = ParseQueryString<"name=John&age=30&city=NYC">;
// { name: "John" } & { age: "30" } & { city: "NYC" }Parse Dot Notation Path
type ParsePath<T extends string> = T extends `${infer Key}.${infer Rest}`
? [Key, ...ParsePath<Rest>]
: [T];
type Path = ParsePath<"user.address.city">; // ["user", "address", "city"]When to Use Template Literal Types
- String validation: Ensure strings match expected patterns
- Key transformation: Rename object keys systematically
- Route typing: Type-safe route parameters
- Event systems: Generate event handler names
- Code generation: Create type-safe string patterns
- API contracts: Ensure URL/path patterns are correct
Common Pitfalls
Complexity Limits
TypeScript has recursion limits. Very deep template literal operations may fail:
// May hit recursion limit with very long strings
type DeepSplit<S extends string> = S extends `${infer H}${infer T}`
? [H, ...DeepSplit<T>]
: [];Greedy Matching
Template literals match greedily:
// This captures everything before the LAST .json
type GetPath<T> = T extends `${infer Path}.json` ? Path : never;
type Test = GetPath<"folder/file.backup.json">;
// "folder/file.backup" (includes the extra .backup)Symbol Keys
Template literals only work with string keys:
type AddPrefix<T, P extends string> = {
// Need to check K extends string to filter out symbols
[K in keyof T as K extends string ? `${P}${K}` : never]: T[K];
};Best Practices
1. Keep patterns simple: Complex recursive patterns are hard to debug 2. Provide fallback types: Handle non-matching cases gracefully 3. Test edge cases: Empty strings, single characters, no matches 4. Consider performance: Deep recursion can slow down type checking 5. Use built-in utilities: Prefer Uppercase, Lowercase, etc. over custom implementations
Type Narrowing
Overview
Type narrowing is TypeScript's ability to refine types based on control flow analysis. When you check a type condition, TypeScript narrows the type within that code block.
Built-in Narrowing
typeof Guards
function processValue(value: string | number) {
if (typeof value === "string") {
// value is string here
return value.toUpperCase();
}
// value is number here
return value.toFixed(2);
}instanceof Guards
function logError(error: Error | string) {
if (error instanceof Error) {
// error is Error here
console.log(error.stack);
} else {
// error is string here
console.log(error);
}
}Truthiness Narrowing
function printName(name: string | null | undefined) {
if (name) {
// name is string here (truthy)
console.log(name.toUpperCase());
}
}Equality Narrowing
function example(x: string | number, y: string | boolean) {
if (x === y) {
// Both are string here (only common type)
console.log(x.toUpperCase());
console.log(y.toUpperCase());
}
}in Operator
interface Fish {
swim: () => void;
}
interface Bird {
fly: () => void;
}
function move(animal: Fish | Bird) {
if ("swim" in animal) {
// animal is Fish here
animal.swim();
} else {
// animal is Bird here
animal.fly();
}
}Discriminated Unions
Use a common property to discriminate between types:
interface Circle {
kind: "circle";
radius: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Triangle {
kind: "triangle";
base: number;
height: number;
}
type Shape = Circle | Rectangle | Triangle;
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
// shape is Circle here
return Math.PI * shape.radius ** 2;
case "rectangle":
// shape is Rectangle here
return shape.width * shape.height;
case "triangle":
// shape is Triangle here
return (shape.base * shape.height) / 2;
}
}Exhaustiveness Checking
Use never to ensure all cases are handled:
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return (shape.base * shape.height) / 2;
default:
// If a new shape is added, this will error
const _exhaustiveCheck: never = shape;
throw new Error(`Unhandled shape: ${_exhaustiveCheck}`);
}
}Custom Type Guards
Type Predicates
Functions that return value is Type:
interface Fish {
swim: () => void;
}
interface Bird {
fly: () => void;
}
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
function move(pet: Fish | Bird) {
if (isFish(pet)) {
// pet is Fish here
pet.swim();
} else {
// pet is Bird here
pet.fly();
}
}Generic Type Guards
function isNotNull<T>(value: T | null | undefined): value is T {
return value !== null && value !== undefined;
}
const values = [1, null, 2, undefined, 3];
const filtered = values.filter(isNotNull);
// filtered is number[]Object Property Check
function hasProperty<T extends object, K extends string>(
obj: T,
key: K,
): obj is T & Record<K, unknown> {
return key in obj;
}
const data: unknown = { name: "Alice" };
if (typeof data === "object" && data !== null && hasProperty(data, "name")) {
// data.name is now accessible
console.log(data.name);
}Assertion Functions
Functions that throw on invalid input:
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new Error(`Expected string, got ${typeof value}`);
}
}
function processInput(input: unknown) {
assertIsString(input);
// input is string here
console.log(input.toUpperCase());
}With Objects
interface User {
id: string;
name: string;
}
function assertIsUser(value: unknown): asserts value is User {
if (
typeof value !== "object" ||
value === null ||
!("id" in value) ||
!("name" in value)
) {
throw new Error("Invalid user object");
}
}
function handleData(data: unknown) {
assertIsUser(data);
// data is User here
console.log(data.name);
}Important: Assertion Function Syntax
Must use function declaration, not arrow functions:
// Error: Assertions require every name in the call target to be
// declared with an explicit type annotation.
const assertString = (value: unknown): asserts value is string => {
if (typeof value !== "string") throw new Error("Not a string");
};
// Correct
function assertString(value: unknown): asserts value is string {
if (typeof value !== "string") throw new Error("Not a string");
}Narrowing with Opaque Types
Combine type predicates with opaque types for validated data:
type ValidEmail = string & { __brand: "ValidEmail" };
function isValidEmail(email: string): email is ValidEmail {
return email.includes("@") && email.includes(".");
}
function sendEmail(email: ValidEmail) {
// We know email has been validated
}
function handleSubmit(email: string) {
if (!isValidEmail(email)) {
throw new Error("Invalid email");
}
// email is ValidEmail here
sendEmail(email);
}Array Filtering with Type Guards
type Item = { type: "a"; value: string } | { type: "b"; count: number };
const items: Item[] = [
{ type: "a", value: "hello" },
{ type: "b", count: 42 },
];
// Filter to specific type
const typeAItems = items.filter(
(item): item is { type: "a"; value: string } => item.type === "a",
);
// typeAItems is { type: "a"; value: string }[]Control Flow Analysis Limitations
TypeScript can't always track narrowing across function calls:
function isString(x: unknown): x is string {
return typeof x === "string";
}
function example(value: string | number) {
const isStr = isString(value);
if (isStr) {
// value is still string | number here!
// TypeScript doesn't narrow based on boolean variables
}
// Must check inline
if (isString(value)) {
// value is string here
}
}Practical Example: API Response Handling
interface SuccessResponse<T> {
status: "success";
data: T;
}
interface ErrorResponse {
status: "error";
error: {
code: string;
message: string;
};
}
type ApiResponse<T> = SuccessResponse<T> | ErrorResponse;
function isSuccess<T>(
response: ApiResponse<T>,
): response is SuccessResponse<T> {
return response.status === "success";
}
async function fetchUser(): Promise<ApiResponse<User>> {
// ...
}
async function handleUser() {
const response = await fetchUser();
if (isSuccess(response)) {
// response.data is User
console.log(response.data.name);
} else {
// response.error is accessible
console.error(response.error.message);
}
}When to Use Each Technique
| Technique | Use Case |
|---|---|
typeof | Primitive type checks |
instanceof | Class instance checks |
in operator | Property existence checks |
| Discriminated unions | Multiple related types with common discriminant |
| Type predicates | Custom narrowing logic |
| Assertion functions | Validation with early error throwing |
Common Pitfalls
Narrowing Doesn't Persist Across Callbacks
function example(value: string | null) {
if (value !== null) {
// value is string here
setTimeout(() => {
// value is string | null again!
// TypeScript is conservative about callbacks
}, 0);
}
}Type Guards Must Return Boolean
// Wrong - doesn't narrow
function isFish(pet: Fish | Bird) {
return "swim" in pet; // Just returns boolean
}
// Correct - narrows the type
function isFish(pet: Fish | Bird): pet is Fish {
return "swim" in pet;
}Be Careful with Complex Conditions
function example(value: { a?: string; b?: number }) {
// This doesn't narrow as expected
if (value.a || value.b) {
// Neither a nor b is guaranteed to exist
}
// Use specific checks
if (value.a !== undefined) {
// value.a is string here
}
}TypeScript Utility Types
Overview
TypeScript provides built-in utility types that transform types in common ways. Mastering these utilities is essential for advanced TypeScript programming.
Parameters<T>
Extracts the parameter types of a function type as a tuple:
function fetchUser(id: string, opts?: { timeout?: number }): Promise<User> {
// ...
}
type FetchUserParams = Parameters<typeof fetchUser>;
// Type: [id: string, opts?: { timeout?: number } | undefined]
// Use in wrapper functions
const fetchUserWithLogging = async (
...args: Parameters<typeof fetchUser>
): Promise<User> => {
console.log("Fetching user:", args[0]);
return fetchUser(...args);
};ReturnType<T>
Extracts the return type of a function type:
function createUser(name: string, email: string) {
return {
id: crypto.randomUUID(),
name,
email,
createdAt: new Date(),
};
}
type User = ReturnType<typeof createUser>;
// Type: { id: string; name: string; email: string; createdAt: Date }Awaited<T>
Unwraps the type inside a Promise (including nested Promises):
type PromiseString = Promise<string>;
type NestedPromise = Promise<Promise<number>>;
type Unwrapped1 = Awaited<PromiseString>; // string
type Unwrapped2 = Awaited<NestedPromise>; // number
// Combine with ReturnType for async functions
async function fetchUser(id: string): Promise<User> {
// ...
}
type FetchUserResult = Awaited<ReturnType<typeof fetchUser>>;
// Type: User (not Promise<User>)Pattern: Wrapping External Library Functions
When extending functions from external libraries that don't export their types:
import { fetchUser } from "external-lib";
// Extract and extend the return type
type FetchUserReturn = Awaited<ReturnType<typeof fetchUser>>;
export const fetchUserWithFullName = async (
...args: Parameters<typeof fetchUser>
): Promise<FetchUserReturn & { fullName: string }> => {
const user = await fetchUser(...args);
return {
...user,
fullName: `${user.firstName} ${user.lastName}`,
};
};Record<Keys, Type>
Creates an object type with specified keys and value type:
type Role = "admin" | "user" | "guest";
type Permissions = Record<Role, string[]>;
const rolePermissions: Permissions = {
admin: ["read", "write", "delete"],
user: ["read", "write"],
guest: ["read"],
};
// Dynamic keys with constraint
function createLookup<K extends string, V>(
keys: K[],
getValue: (key: K) => V,
): Record<K, V> {
const result = {} as Record<K, V>;
for (const key of keys) {
result[key] = getValue(key);
}
return result;
}Partial<T>
Makes all properties optional:
interface User {
id: string;
name: string;
email: string;
}
type UpdateUserInput = Partial<User>;
// Type: { id?: string; name?: string; email?: string }
function updateUser(id: string, updates: Partial<User>): User {
// ...
}
updateUser("123", { name: "New Name" }); // OK - only updating nameRequired<T>
Makes all properties required (opposite of Partial):
interface Config {
host?: string;
port?: number;
debug?: boolean;
}
type RequiredConfig = Required<Config>;
// Type: { host: string; port: number; debug: boolean }Omit<T, Keys>
Creates a type by omitting specified properties:
interface User {
id: string;
name: string;
email: string;
password: string;
}
type PublicUser = Omit<User, "password">;
// Type: { id: string; name: string; email: string }
type CreateUserInput = Omit<User, "id">;
// Type: { name: string; email: string; password: string }Pick<T, Keys>
Creates a type by picking specified properties (opposite of Omit):
interface User {
id: string;
name: string;
email: string;
password: string;
createdAt: Date;
}
type UserCredentials = Pick<User, "email" | "password">;
// Type: { email: string; password: string }Exclude<T, U> and Extract<T, U>
Work with union types:
type AllColors = "red" | "green" | "blue" | "yellow";
type PrimaryColors = Extract<AllColors, "red" | "blue">;
// Type: "red" | "blue"
type NonPrimaryColors = Exclude<AllColors, "red" | "blue">;
// Type: "green" | "yellow"NonNullable<T>
Removes null and undefined from a type:
type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>;
// Type: stringCreating a Reusable Wrapper Type
Combine utilities to create reusable type helpers:
// A type that wraps any async function, extending its return type
type WrapFunction<TFunc extends (...args: any) => any, TAdditional = {}> = (
...args: Parameters<TFunc>
) => Promise<Awaited<ReturnType<TFunc>> & TAdditional>;
// Usage
import { fetchUser, fetchPost } from "external-lib";
const fetchUserWithMeta: WrapFunction<
typeof fetchUser,
{ meta: { fetchedAt: Date } }
> = async (...args) => {
const user = await fetchUser(...args);
return {
...user,
meta: { fetchedAt: new Date() },
};
};When to Use Each Utility
| Utility | Use Case |
|---|---|
Parameters<T> | Wrapping functions, creating function variants |
ReturnType<T> | Extracting return types when not explicitly exported |
Awaited<T> | Unwrapping Promise types |
Record<K, V> | Creating object types with dynamic keys |
Partial<T> | Update/patch operations |
Required<T> | Ensuring all config options are provided |
Omit<T, K> | Removing sensitive or internal fields |
Pick<T, K> | Creating focused subsets of types |
Exclude<T, U> | Filtering union types |
Extract<T, U> | Selecting from union types |
NonNullable<T> | Removing null/undefined after validation |
Common Pitfalls
Using ReturnType on Async Functions
async function getData(): Promise<string[]> {
return ["data"];
}
// This gives Promise<string[]>, not string[]
type Wrong = ReturnType<typeof getData>; // Promise<string[]>
// Use Awaited to unwrap
type Right = Awaited<ReturnType<typeof getData>>; // string[]Forgetting typeof for Runtime Functions
function myFunc(x: number): string {
return String(x);
}
// Wrong - myFunc is a value, not a type
type Params = Parameters<myFunc>; // Error
// Correct - use typeof
type Params = Parameters<typeof myFunc>; // [x: number]{
"name": "mcollina/typescript-magician",
"version": "0.1.0",
"private": false,
"summary": "Designs complex generic types, refactors `any` types to strict alternatives, creates type guards and utility types, and resolves TypeScript compiler errors. Use when the user asks about TypeScript (TS) types, generics, type inference, type guards, removing `any` types, strict typing, type errors, `infer`, `extends`, conditional types, mapped types, template literal types, branded/opaque types, or utility types like `Partial`, `Record`, `ReturnType`, and `Awaited`.",
"skills": {
"typescript-magician": {
"path": "SKILL.md"
}
}
}