TypeScript Type System
Advanced TypeScript type-system mechanics — union and intersection types, conditional types with `infer`, mapped types, template literal types, generics and their constraints, index signatures, type assertions, and extracting types from other types (return types, array elements, promise results). Use when designing a complex generic, modeling a type that transforms based on another type, extracting a type from a function or structure with `infer`, or deciding between a type assertion and a real narrowing check.
npx skills add github:pantheon-org/tekhne --skill development--typescript-advanced--type-systemThe type-level mechanics that everything else in this skill family builds on: unions, conditionals, mapped types, template literals, generics, and type-level extraction with infer.
Mindset
Model the problem in types before reaching for a runtime check — a type that makes an invalid combination unrepresentable is a compile-time guarantee, while a runtime check for the same thing only protects the paths that get exercised. Prefer deriving a type from another type (ReturnType<typeof fn>, a mapped type, a conditional extraction with infer) over hand-writing a parallel type that can drift out of sync with the thing it describes. When a conditional type distributes over a union unexpectedly, that is usually the type system doing exactly what it’s told — wrap the checked type in a tuple ([T] extends [U]) to opt out of distribution rather than restructuring the type to dodge it. A type assertion (as) is a claim, not a check; only reach for one when you have already established the fact some other way (e.g. immediately after a runtime guard), never as a way to silence an error you haven’t investigated.
When to Apply
Use this skill when:
- Modeling a union or intersection type, including discriminated unions at the type-definition level
- Writing a conditional type, especially one using
inferto extract a nested type - Building a mapped type or remapping its keys with an
asclause - Constructing a template literal type (e.g. typed route parameters)
- Designing a generic function or type with meaningful constraints
- Extracting a type from a function, array, promise, or constructor
- Deciding whether a value needs an index signature or a fixed shape
- Choosing between a type assertion and a real narrowing check
Use When
- “How do I extract the return type / element type / awaited type from this?”
- “How should I model this union or generic type?”
- “How do I build a type that changes shape based on another type?”
- “What’s the difference between
asand a type guard here?”
Scope
In Scope
- Union, intersection, conditional, mapped, and template literal types.
- Generics and their constraints.
- Index signatures and type assertions.
- Type-level extraction with
infer(return types, array elements, promise values, tuple destructuring, constructor/instance types).
Out of Scope
- Runtime narrowing and type guards (
typeof,instanceof, custom predicates) — see the siblingtypescript-type-guardsskill, which also owns exhaustiveness checking. - Built-in utility types that wrap these mechanics for common cases (
Partial,Pick,ReturnTypeas a ready-made utility) — see the siblingtypescript-utility-typesskill for the applied, ready-to-use versions. - Design patterns that use these mechanics structurally — see the sibling
typescript-design-patternsskill.
When NOT to Use
Do not use this skill for a runtime check — narrowing a value at runtime with typeof/instanceof/a custom predicate belongs in typescript-type-guards. Do not use it as a substitute for tsc --noEmit; a type that looks correct in isolation can still fail to apply as expected against real call sites.
Quick Commands
Type Check
npx tsc --noEmit
Type Check Single Entry
npx tsc --noEmit src/index.ts
Anti-Patterns
NEVER silence a type error with an unchecked assertion instead of modeling the type correctly
WHY: an as assertion bypasses the compiler without a matching runtime guarantee; the error it silences almost always points at a type that needs to be modeled more precisely, not asserted away.
BAD:
const id = getValue() as string;
GOOD:
type Result = { kind: "ok"; value: string } | { kind: "error"; message: string };
function unwrap(result: Result): string {
if (result.kind === "error") throw new Error(result.message);
return result.value;
}
NEVER hand-write a type that duplicates what infer or a mapped type could derive
WHY: a hand-written parallel type drifts out of sync with the source it was copied from the first time either one changes.
BAD:
function getUser() { return { id: 1, name: "Alice" }; }
interface User { id: number; name: string; } // duplicates getUser's actual return shape
GOOD:
function getUser() { return { id: 1, name: "Alice" }; }
type User = ReturnType<typeof getUser>; // stays correct if getUser changes
NEVER leave a generic parameter unconstrained when the type only makes sense for a shape
WHY: an unconstrained <T> accepts anything, including values the type’s own logic assumes won’t appear.
BAD:
type PropType<T, K> = T extends { [P in K]: infer V } ? V : never; // K unconstrained
GOOD:
type PropType<T, K extends keyof T> = T extends { [P in K]: infer V } ? V : never;
References
| File | Covers |
|---|---|
references/unions-intersections.md |
Union and intersection types |
references/conditional-types.md |
Conditional types, infer, distributive behavior |
references/mapped-types.md |
Mapped types and key remapping mechanics |
references/template-literal-types.md |
Template literal types (e.g. typed route parameters) |
references/generics.md |
Generic constraints and type parameters |
references/index-signatures.md |
Index signatures and dynamic properties |
references/type-assertions.md |
as assertions and type compatibility |
references/infer-extraction.md |
Extracting return types, parameters, array elements, promise values, tuple elements, and instance/this types with infer |
Explicit NEVER/WHY/consequence triplets with side-by-side bad/good examples. Generic warnings score low — concrete failure modes with real impact (security, correctness, performance) score high.
Runtime validation via eval scenarios proving the skill actually changes agent behaviour. Requires evals/ directory, instructions.json, ≥3 scenarios, and ≥80% instruction coverage in summary.json.
Appropriate rigidity for the skill type: strict rules for critical/security skills, balanced steps for process skills, flexible options for tool skills. Mismatched calibration (too rigid or too loose) reduces score.
Expert-only knowledge the AI couldn't derive on its own — domain-specific patterns, production gotchas, decision frameworks, and non-obvious tool behaviour. High scores mean <5% redundancy with what the model already knows.
A clear philosophy plus numbered step-by-step workflows with explicit entry/exit points. Should answer: what is the core principle, how to execute it, and when (and when not) to apply it.
Rich activation keywords and trigger scenarios in the description so the skill fires when needed. Best descriptions read like an exhaustive list of the tasks and concepts that should invoke this skill.
Copy-paste-ready code, complete runnable examples, real file paths and commands. Pseudocode and abstract descriptions score low — concrete, executable content scores high.
SKILL.md as a <100-line navigation hub pointing to focused references/ files. Content should be lazy-loaded on demand, not front-loaded. Includes actionable 'When to Use' conditions per reference.
Proper frontmatter, single-task focus, activation keywords, and cross-harness portability. Skills must work across 40+ agentic harnesses without hardcoded paths or agent-specific references.
No eval scenarios available.
Conditional Types
Basic Conditional Types
Types that select one of two possible types based on a condition:
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
// Useful for filtering
type NonNullable<T> = T extends null | undefined ? never : T;
The infer Keyword
Extract types from other types:
// Extract return type from function
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Fn = () => string;
type Result = ReturnType<Fn>; // string
// Extract array element type
type ElementType<T> = T extends (infer E)[] ? E : never;
type Items = ElementType<string[]>; // string
// Extract Promise result type
type Awaited<T> = T extends Promise<infer U> ? U : T;
type P = Awaited<Promise<string>>; // string
Distributive Conditional Types
Conditional types distribute over union types:
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>;
// Distributes to: ToArray<string> | ToArray<number>
// Results in: string[] | number[]
// Prevent distribution with tuple wrapper
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Result2 = ToArrayNonDist<string | number>;
// Results in: (string | number)[]
Nested Conditional Types
Chain multiple conditions:
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 A = TypeName<string>; // "string"
type B = TypeName<() => void>; // "function"
Inferring Multiple Types
// Extract function parameter types
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
type Fn = (a: string, b: number) => void;
type Params = Parameters<Fn>; // [string, number]
// Extract constructor parameters
type ConstructorParameters<T> = T extends new (...args: infer P) => any
? P
: never;
class User {
constructor(name: string, age: number) {}
}
type UserParams = ConstructorParameters<typeof User>; // [string, number]
Practical Examples
Filter Union Types
type Exclude<T, U> = T extends U ? never : T;
type Extract<T, U> = T extends U ? T : never;
type Numbers = Exclude<string | number | boolean, string>; // number | boolean
type Strings = Extract<string | number | boolean, string>; // string
Deep Readonly
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? DeepReadonly<T[P]>
: T[P];
};
interface Config {
database: {
host: string;
port: number;
};
}
type ReadonlyConfig = DeepReadonly<Config>;
// All properties (including nested) are readonly
Flatten Array Types
type Flatten<T> = T extends Array<infer U> ? U : T;
type Flat = Flatten<string[]>; // string
type NotFlat = Flatten<string>; // string
Function Type Manipulation
// Make all parameters optional
type PartialParameters<T extends (...args: any[]) => any> =
T extends (...args: infer P) => infer R
? (...args: Partial<P>) => R
: never;
// Add error parameter to function
type WithError<T extends (...args: any[]) => any> =
T extends (...args: infer P) => infer R
? (...args: [...P, Error?]) => R
: never;
Advanced Patterns
Type-Safe API Routes
type RouteParams<T extends string> =
T extends `${infer _Start}:${infer Param}/${infer Rest}`
? { [K in Param | keyof RouteParams<Rest>]: string }
: T extends `${infer _Start}:${infer Param}`
? { [K in Param]: string }
: {};
type Params = RouteParams<'/users/:userId/posts/:postId'>;
// { userId: string; postId: string }
Recursive Conditional Types
type JSONValue =
| string
| number
| boolean
| null
| JSONValue[]
| { [key: string]: JSONValue };
type JSONCompatible<T> =
T extends JSONValue ? T :
T extends { toJSON(): infer U } ? U :
never;
Best Practices
- Use conditional types for type transformation
- Leverage
inferto extract type information - Be aware of distributive behavior
- Use tuple wrappers to prevent distribution when needed
- Keep conditions readable - extract to separate types if complex
Common Pitfalls
- Don’t forget distribution behavior over unions
- Avoid overly complex nested conditionals
- Be careful with
neverin conditional types - Don’t confuse
inferplacement in different contexts - Watch out for infinite recursion in recursive conditional types
Performance Considerations
- Conditional types can be expensive for the compiler
- Deeply nested conditional types slow down type checking
- Consider caching complex conditional type results
- Limit recursion depth (TypeScript has limits)
Generics
Basic Generics
Type variables that allow components to work with any type:
function identity<T>(value: T): T {
return value;
}
const num = identity(42); // T inferred as number
const str = identity("hello"); // T inferred as string
Generic Constraints
Restrict type parameters with extends:
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(value: T): void {
console.log(value.length);
}
logLength("hello"); // OK
logLength([1, 2, 3]); // OK
logLength({ length: 5 }); // OK
// logLength(42); // Error: number has no length
Multiple Type Parameters
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const result = pair("age", 30); // [string, number]
// With constraints
function merge<T extends object, U extends object>(
obj1: T,
obj2: U
): T & U {
return { ...obj1, ...obj2 };
}
Generic Interfaces
interface Container<T> {
value: T;
map<U>(fn: (value: T) => U): Container<U>;
}
interface Response<T> {
data: T;
status: number;
headers: Record<string, string>;
}
const userResponse: Response<User> = {
data: { id: 1, name: 'Alice' },
status: 200,
headers: {}
};
Generic Classes
class Box<T> {
constructor(private value: T) {}
getValue(): T {
return this.value;
}
map<U>(fn: (value: T) => U): Box<U> {
return new Box(fn(this.value));
}
}
const numBox = new Box(42);
const strBox = numBox.map(n => n.toString());
Default Type Parameters
interface Config<T = string> {
value: T;
}
const stringConfig: Config = { value: "hello" };
const numberConfig: Config<number> = { value: 42 };
// Useful for API responses
interface ApiResponse<T = unknown> {
data: T;
error?: string;
}
Generic Type Aliases
type Result<T, E = Error> =
| { success: true; value: T }
| { success: false; error: E };
type Nullable<T> = T | null;
type AsyncFn<T, Args extends any[]> = (...args: Args) => Promise<T>;
Constrained Keys
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: 'Alice', age: 30 };
const name = getProperty(user, 'name'); // string
const age = getProperty(user, 'age'); // number
// getProperty(user, 'invalid'); // Error
Generic Inference
TypeScript infers generic types from usage:
// Inferred from argument
function wrap<T>(value: T) {
return { value };
}
const wrapped = wrap(42); // { value: number }
// Inferred from return type
function parse<T>(json: string): T {
return JSON.parse(json);
}
const user: User = parse('{"name":"Alice"}'); // T inferred as User
Advanced Generic Patterns
Builder Pattern
class QueryBuilder<T> {
private conditions: string[] = [];
where(condition: string): QueryBuilder<T> {
this.conditions.push(condition);
return this;
}
execute(): T[] {
// Execute query
return [];
}
}
Type-Safe Event Emitter
type EventMap = Record<string, any>;
class TypedEmitter<Events extends EventMap> {
on<K extends keyof Events>(
event: K,
handler: (payload: Events[K]) => void
): void {
// Implementation
}
emit<K extends keyof Events>(event: K, payload: Events[K]): void {
// Implementation
}
}
interface AppEvents {
'user:login': { userId: string; timestamp: Date };
'user:logout': { userId: string };
}
const emitter = new TypedEmitter<AppEvents>();
emitter.on('user:login', (payload) => {
// payload is correctly typed as { userId: string; timestamp: Date }
});
Best Practices
- Use descriptive generic names (T for type, K for key, V for value)
- Add constraints to generics when possible
- Leverage type inference instead of explicit type arguments
- Use default type parameters for common cases
- Avoid over-constraining generics
Common Pitfalls
- Don’t use generics when concrete types suffice
- Avoid too many type parameters (>3 is often a code smell)
- Don’t forget to constrain when needed
- Avoid losing type information by being too generic
Index Signatures & Dynamic Properties
Basic Index Signatures
Allow dynamic property access with consistent types.
interface StringDictionary {
[key: string]: string;
}
const dict: StringDictionary = {
name: 'John',
role: 'Developer'
};
console.log(dict['name']); // ✅ string
console.log(dict['anything']); // ✅ string (might be undefined at runtime!)
Numeric Index Signatures
interface NumberArray {
[index: number]: string;
}
const arr: NumberArray = ['a', 'b', 'c'];
console.log(arr[0]); // ✅ string
Mixed with Known Properties
interface Config {
name: string;
version: number;
[key: string]: string | number; // Known properties must match
}
const config: Config = {
name: 'app',
version: 1,
author: 'John', // ✅ Additional properties allowed
year: 2024
};
Rule: Known property types must be assignable to the index signature type.
Template Literal in Index Signatures (TypeScript 4.4+)
interface Getters {
[K: `get${string}`]: () => any;
}
const obj: Getters = {
getName: () => 'John',
getAge: () => 30,
// setName: () => {} // ❌ Error: must start with 'get'
};
Record Utility Type
// Shorthand for index signature
type StringMap = Record<string, string>;
// With specific keys
type Roles = Record<'admin' | 'user' | 'guest', string[]>;
const permissions: Roles = {
admin: ['read', 'write', 'delete'],
user: ['read', 'write'],
guest: ['read']
};
Restricting Keys
// Only specific keys allowed
type AllowedKeys = 'name' | 'age' | 'email';
type UserData = Record<AllowedKeys, string>;
const user: UserData = {
name: 'John',
age: '30',
email: 'john@example.com'
// role: 'admin' // ❌ Error: role not in AllowedKeys
};
Combining with Mapped Types
type Optional<T> = {
[K in keyof T]?: T[K];
};
type WithDefaults<T> = {
[K in keyof T]: T[K] | null;
};
interface User {
name: string;
age: number;
}
type PartialUser = Optional<User>;
// { name?: string; age?: number; }
type NullableUser = WithDefaults<User>;
// { name: string | null; age: number | null; }
noUncheckedIndexedAccess
Stricter type checking for index access:
// tsconfig.json
{
"compilerOptions": {
"noUncheckedIndexedAccess": true
}
}
interface Dict {
[key: string]: string;
}
const dict: Dict = { name: 'John' };
const value = dict['anything']; // Type: string | undefined (safer!)
Best Practices
- Use Record<K, V> for simple cases - More concise than index signatures
- Enable noUncheckedIndexedAccess - Catches potential undefined access
- Prefer known properties - Index signatures are for truly dynamic data
- Consider Map for dynamic data - Better runtime guarantees
- Document expected keys - Comment what keys are expected
Common Patterns
Environment Variables
interface ProcessEnv {
NODE_ENV: 'development' | 'production' | 'test';
[key: string]: string | undefined;
}
declare const process: {
env: ProcessEnv;
};
API Response with Dynamic Fields
interface ApiResponse<T> {
status: number;
data: T;
[key: string]: unknown; // Additional metadata
}
Configuration with Sections
interface AppConfig {
[section: string]: {
[key: string]: string | number | boolean;
};
}
const config: AppConfig = {
database: {
host: 'localhost',
port: 5432
},
cache: {
enabled: true,
ttl: 3600
}
};
Common Pitfalls
- Assuming indexed values exist - Always check for undefined
- Too permissive types - Index signature
anydefeats type safety - Mixing incompatible known properties - Known properties must match index signature type
- Using index signatures for fixed shapes - Use explicit properties instead
Type Inference with infer
Overview
The infer keyword allows extracting types from other types within conditional type expressions, enabling powerful type-level programming.
Extracting Function Return Type
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function getUser() {
return { id: 1, name: "Alice" };
}
type User = ReturnType<typeof getUser>;
// { id: number; name: string; }
Extracting Function Parameters
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
function createUser(name: string, age: number) {
return { name, age };
}
type CreateUserParams = Parameters<typeof createUser>;
// [name: string, age: number]
Extracting Array Element Type
type ArrayElement<T> = T extends (infer E)[] ? E : never;
type StringArray = string[];
type Element = ArrayElement<StringArray>; // string
type MixedArray = (string | number)[];
type MixedElement = ArrayElement<MixedArray>; // string | number
Extracting Promise Value
type Awaited<T> = T extends Promise<infer U> ? U : T;
async function fetchUser() {
return { id: 1, name: "Alice" };
}
type User = Awaited<ReturnType<typeof fetchUser>>;
// { id: number; name: string; }
Extracting Object Property Type
type PropType<T, K extends keyof T> = T extends { [P in K]: infer V }
? V
: never;
interface User {
id: number;
name: string;
email: string;
}
type EmailType = PropType<User, "email">; // string
Extracting Constructor Parameters
type ConstructorParameters<T> = T extends new (...args: infer P) => any
? P
: never;
class User {
constructor(public name: string, public age: number) {}
}
type UserConstructorParams = ConstructorParameters<typeof User>;
// [name: string, age: number]
Extracting Instance Type
type InstanceType<T> = T extends new (...args: any[]) => infer R ? R : never;
class Database {
connect() { }
query(sql: string) { }
}
type DB = InstanceType<typeof Database>;
// Database
Nested infer
type DeepArrayElement<T> = T extends (infer E)[]
? E extends (infer F)[]
? F
: E
: never;
type NestedArray = string[][];
type Element = DeepArrayElement<NestedArray>; // string
Inferring Tuple Elements
type First<T> = T extends [infer F, ...any[]] ? F : never;
type Rest<T> = T extends [any, ...infer R] ? R : never;
type Tuple = [string, number, boolean];
type FirstElement = First<Tuple>; // string
type RestElements = Rest<Tuple>; // [number, boolean]
Inferring Function This Type
type ThisType<T> = T extends (this: infer U, ...args: any[]) => any
? U
: never;
interface Database {
query(this: Database, sql: string): void;
}
type DB = ThisType<Database["query"]>; // Database
Best Practices
- Use infer for extracting types from complex structures
- Combine infer with distributive conditional types
- Name inferred types descriptively (U, V, R for Return, P for Parameters)
- Consider built-in utility types before creating custom ones
- Document complex infer usage
Common Pitfalls
- infer only works within extends clauses of conditional types
- Multiple infer of same type variable creates union
- Can’t infer constrained generic types directly
- Over-nesting makes types hard to understand
Mapped Types
Basic Mapped Types
Transform properties of existing types:
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type Partial<T> = {
[P in keyof T]?: T[P];
};
type Required<T> = {
[P in keyof T]-?: T[P]; // Remove optional modifier
};
interface User {
name: string;
age?: number;
}
type ReadonlyUser = Readonly<User>;
// { readonly name: string; readonly age?: number }
type CompleteUser = Required<User>;
// { name: string; age: number }
Key Remapping
Transform property keys with as:
type Getters<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// {
// getName: () => string;
// getAge: () => number;
// }
Filtering Properties
Use conditional types with never to filter:
type PickByType<T, U> = {
[P in keyof T as T[P] extends U ? P : never]: T[P];
};
interface Mixed {
name: string;
age: number;
active: boolean;
score: number;
}
type StringProps = PickByType<Mixed, string>;
// { name: string }
type NumberProps = PickByType<Mixed, number>;
// { age: number; score: number }
Modifiers
Add or remove modifiers:
// Remove readonly
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
// Remove optional
type Concrete<T> = {
[P in keyof T]-?: T[P];
};
// Add both
type Frozen<T> = {
readonly [P in keyof T]-?: T[P];
};
Nested Mapped Types
Recursively transform nested objects:
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object
? DeepPartial<T[P]>
: T[P];
};
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? DeepReadonly<T[P]>
: T[P];
};
interface Config {
database: {
host: string;
port: number;
credentials: {
username: string;
password: string;
};
};
}
type PartialConfig = DeepPartial<Config>;
// All properties at all levels are optional
Template Literal Key Remapping
Combine with template literal types:
type EventHandlers<T> = {
[P in keyof T as `on${Capitalize<string & P>}Change`]: (
value: T[P]
) => void;
};
interface Form {
username: string;
email: string;
age: number;
}
type FormHandlers = EventHandlers<Form>;
// {
// onUsernameChange: (value: string) => void;
// onEmailChange: (value: string) => void;
// onAgeChange: (value: number) => void;
// }
Conditional Mapped Types
type Nullable<T> = {
[P in keyof T]: T[P] | null;
};
type NullableStrings<T> = {
[P in keyof T]: T[P] extends string ? T[P] | null : T[P];
};
interface Data {
name: string;
age: number;
email: string;
}
type Result = NullableStrings<Data>;
// {
// name: string | null;
// age: number;
// email: string | null;
// }
Advanced Patterns
Proxied Properties
type Proxied<T> = {
[P in keyof T]: {
get(): T[P];
set(value: T[P]): void;
};
};
Prefixed Properties
type Prefix<T, P extends string> = {
[K in keyof T as `${P}${Capitalize<string & K>}`]: T[K];
};
type User = { name: string; age: number };
type OldUser = Prefix<User, 'old'>;
// { oldName: string; oldAge: number }
Omit by Type
type OmitByType<T, U> = {
[P in keyof T as T[P] extends U ? never : P]: T[P];
};
interface Mixed {
name: string;
age: number;
active: boolean;
}
type NoStrings = OmitByType<Mixed, string>;
// { age: number; active: boolean }
Readonly Keys
type ReadonlyKeys<T> = {
[P in keyof T]-?: (<F>() => F extends { [Q in P]: T[P] } ? 1 : 2) extends
(<F>() => F extends { -readonly [Q in P]: T[P] } ? 1 : 2)
? never
: P;
}[keyof T];
Required Keys
type RequiredKeys<T> = {
[K in keyof T]-?: {} extends Pick<T, K> ? never : K;
}[keyof T];
type OptionalKeys<T> = {
[K in keyof T]-?: {} extends Pick<T, K> ? K : never;
}[keyof T];
Practical Examples
Form State
type FormState<T> = {
[P in keyof T]: {
value: T[P];
error?: string;
touched: boolean;
};
};
Async Versions
type Promisify<T> = {
[P in keyof T]: T[P] extends (...args: infer A) => infer R
? (...args: A) => Promise<R>
: T[P];
};
Builder Pattern
type Builder<T> = {
[P in keyof T as `set${Capitalize<string & P>}`]: (value: T[P]) => Builder<T>;
} & {
build(): T;
};
Best Practices
- Use built-in mapped types (Partial, Readonly, Pick, Omit) when possible
- Leverage key remapping for type-safe transformations
- Combine with conditional types for filtering
- Use template literals for systematic renaming
- Be careful with recursive mapped types (performance)
Common Pitfalls
- Don’t map over union types - use distributive conditional types instead
- Avoid overly complex key remapping logic
- Be careful with recursive depth limits
- Don’t forget about modifier removal (-readonly, -?)
- Watch out for preserving optional modifiers
Performance Considerations
- Mapped types can be expensive for large types
- Recursive mapped types multiply compilation time
- Consider caching results for frequently-used transformations
- Keep nesting depth reasonable
Template Literal Types
Basic Template Literals
Create types from string templates:
type World = "world";
type Greeting = `hello ${World}`; // "hello world"
type EmailDomain = "gmail.com" | "yahoo.com";
type Email = `user@${EmailDomain}`;
// "user@gmail.com" | "user@yahoo.com"
String Manipulation Utilities
Built-in type utilities for string transformation:
type Uppercase<S extends string> = intrinsic;
type Lowercase<S extends string> = intrinsic;
type Capitalize<S extends string> = intrinsic;
type Uncapitalize<S extends string> = intrinsic;
type Loud = Uppercase<"hello">; // "HELLO"
type Quiet = Lowercase<"WORLD">; // "world"
type Proper = Capitalize<"john">; // "John"
type Lower = Uncapitalize<"Smith">; // "smith"
Event Handler Generation
type EventNames = "click" | "focus" | "blur";
type EventHandlers = `on${Capitalize<EventNames>}`;
// "onClick" | "onFocus" | "onBlur"
type PropEventHandler<T extends string> = `on${Capitalize<T>}Changed`;
type FormEvents = PropEventHandler<"username" | "email">;
// "onUsernameChanged" | "onEmailChanged"
Path Building
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type Endpoint = "users" | "posts" | "comments";
type ApiRoute = `/${Endpoint}`;
// "/users" | "/posts" | "/comments"
type RestEndpoint = `${HttpMethod} /${Endpoint}`;
// "GET /users" | "POST /users" | ... (12 combinations)
Pattern Matching with infer
Extract parts from template literal types:
type ExtractRouteParams<T extends string> =
T extends `${infer _Start}:${infer Param}/${infer Rest}`
? Param | ExtractRouteParams<Rest>
: T extends `${infer _Start}:${infer Param}`
? Param
: never;
type Params = ExtractRouteParams<"/users/:userId/posts/:postId">;
// "userId" | "postId"
CamelCase Conversion
type CamelCase<S extends string> =
S extends `${infer First}_${infer Rest}`
? `${Lowercase<First>}${Capitalize<CamelCase<Rest>>}`
: Lowercase<S>;
type Result = CamelCase<"hello_world_example">;
// "helloWorldExample"
Type-Safe CSS Properties
type CSSUnits = "px" | "em" | "rem" | "%";
type CSSValue<T extends number> = `${T}${CSSUnits}`;
type Width = CSSValue<100>; // "100px" | "100em" | "100rem" | "100%"
// With specific properties
type Padding = CSSValue<0 | 4 | 8 | 16>;
Database Query Builder
type TableName = "users" | "posts" | "comments";
type Operation = "select" | "insert" | "update" | "delete";
type Query = `${Operation} from ${TableName}`;
// Type-safe query builder
type WhereClause<T extends string> = `where ${T}`;
type FullQuery<T extends TableName, C extends string> =
`select from ${T} ${WhereClause<C>}`;
Accessor Generation
type Accessor<T, K extends keyof T> = {
[P in K as `get${Capitalize<string & P>}`]: () => T[P];
} & {
[P in K as `set${Capitalize<string & P>}`]: (value: T[P]) => void;
};
interface User {
name: string;
age: number;
}
type UserAccessors = Accessor<User, keyof User>;
// {
// getName: () => string;
// setName: (value: string) => void;
// getAge: () => number;
// setAge: (value: number) => void;
// }
Environment Variables
type EnvPrefix = "VITE_" | "NEXT_PUBLIC_";
type EnvVar<T extends string> = `${EnvPrefix}${Uppercase<T>}`;
type AppEnvVars = EnvVar<"api_url" | "api_key">;
// "VITE_API_URL" | "VITE_API_KEY" | "NEXT_PUBLIC_API_URL" | "NEXT_PUBLIC_API_KEY"
// Type-safe env access
declare const process: {
env: Record<AppEnvVars, string>;
};
SQL Table Schema
type Column = "id" | "name" | "email" | "created_at";
type SqlType = "INTEGER" | "TEXT" | "TIMESTAMP";
type ColumnDef = `${Column} ${SqlType}`;
type TableSchema = {
[K in Column]: ColumnDef;
};
Advanced Patterns
URL Builder
type Protocol = "http" | "https";
type Domain = string;
type Path = string;
type Url = `${Protocol}://${Domain}/${Path}`;
type ApiUrl<E extends string> = `https://api.example.com/${E}`;
Versioned API Endpoints
type Version = "v1" | "v2" | "v3";
type Resource = "users" | "posts";
type VersionedEndpoint = `/api/${Version}/${Resource}`;
// "/api/v1/users" | "/api/v1/posts" | "/api/v2/users" | ...
Type-Safe Event Names
type DomainEvent<T extends string> = `domain:${T}`;
type SystemEvent<T extends string> = `system:${T}`;
type UserEvents = DomainEvent<"user:created" | "user:updated">;
// "domain:user:created" | "domain:user:updated"
Best Practices
- Use template literals for systematic naming patterns
- Combine with mapped types for property generation
- Leverage string manipulation utilities
- Use
inferfor pattern extraction - Keep template expressions readable
Common Pitfalls
- Don’t create excessively large union types (combinatorial explosion)
- Be aware of recursion limits in pattern matching
- Avoid overly complex template expressions
- Remember string manipulation utilities are case-sensitive
- Watch out for empty string edge cases
Performance Considerations
- Template literal types can create very large unions
- Each combination is expanded at compile time
- Limit the number of union members in template positions
- Consider using string branding instead for open-ended strings
Type Assertions & Type Compatibility
Type Assertions
Type assertions tell TypeScript to treat a value as a specific type.
as Syntax (Preferred)
const value: unknown = "hello";
const strLength = (value as string).length;
// DOM elements
const input = document.querySelector('input') as HTMLInputElement;
input.value = 'text';
Angle Bracket Syntax (JSX-incompatible)
const value: unknown = "hello";
const strLength = (<string>value).length;
Note: Use as syntax - it works in JSX/TSX files.
Non-null Assertion (!)
function getUser(id: string) {
const user = users.find(u => u.id === id);
// Assert user is not null/undefined
return user!.name;
}
// Optional chaining is safer
return users.find(u => u.id === id)?.name;
Warning: Only use ! when you’re certain the value exists. Prefer optional chaining.
Const Assertions
// Infer literal types instead of widening
const config = {
endpoint: '/api/users',
method: 'GET'
} as const;
// Type: { readonly endpoint: "/api/users"; readonly method: "GET"; }
// Array becomes readonly tuple
const colors = ['red', 'green', 'blue'] as const;
// Type: readonly ["red", "green", "blue"]
// Use in function parameters
function request<T extends string>(method: T) {
// method is the literal type, not string
}
request(config.method); // ✅ method is "GET"
Satisfies Operator (TypeScript 4.9+)
// Validate type without widening
const config = {
endpoint: '/api/users',
method: 'GET',
timeout: 5000
} satisfies Config;
// config.method is still "GET", not string
// Unlike 'as Config', satisfies preserves literal types
type Color = { r: number; g: number; b: number } | string;
const palette = {
red: { r: 255, g: 0, b: 0 },
blue: '#0000FF'
} satisfies Record<string, Color>;
// palette.red.r is accessible (object structure preserved)
// With 'as Record<string, Color>', red.r would error
Type Compatibility
TypeScript uses structural typing (shape-based).
Structural Typing
interface Point {
x: number;
y: number;
}
class Point2D {
constructor(public x: number, public y: number) {}
}
const point: Point = new Point2D(10, 20); // ✅ Compatible
Function Compatibility
type Handler = (msg: string, code: number) => void;
// ✅ Fewer parameters is compatible
const handler: Handler = (msg: string) => {
console.log(msg);
};
// ❌ More parameters is not compatible
const handler2: Handler = (msg: string, code: number, extra: boolean) => {};
Bivariance in Method Parameters
interface Animal { name: string; }
interface Dog extends Animal { breed: string; }
class Vet {
treat(animal: Animal) {}
}
class DogVet extends Vet {
// ⚠️ Allowed but unsound
treat(dog: Dog) {}
}
Best Practices
- Prefer type guards over assertions - Type guards are type-safe
- Use
satisfiesfor validation without widening - Preserves literal types - Const assertions for literal types - Use
as constfor immutable literal types - Avoid non-null assertion (!) - Use optional chaining or type guards instead
- Double assertions as last resort -
value as unknown as TargetTypeindicates design issue
Common Pitfalls
- Overusing
as- Defeats type checking, prefer narrowing - Non-null assertion on uncertain values - Can cause runtime errors
- Ignoring structural compatibility - Extra properties can cause issues with fresh object literals
Union and Intersection Types
Union Types
Represent values that can be one of several types:
type Result = Success | Failure;
type ID = string | number;
type Status = 'pending' | 'approved' | 'rejected';
function handleResult(result: Result) {
// Type narrowing required to access specific properties
if ('data' in result) {
console.log(result.data);
}
}
Intersection Types
Combine multiple types into one:
type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged;
const person: Person = {
name: 'Alice',
age: 30
};
// Useful for mixins
type Timestamped = { createdAt: Date; updatedAt: Date };
type User = Person & Timestamped;
Discriminated Unions
Union types with a common discriminant property for type-safe narrowing:
type Success = {
kind: 'success';
data: string;
};
type Failure = {
kind: 'failure';
error: Error;
};
type Result = Success | Failure;
function handleResult(result: Result) {
switch (result.kind) {
case 'success':
console.log(result.data); // TypeScript knows this is Success
break;
case 'failure':
console.error(result.error); // TypeScript knows this is Failure
break;
}
}
State Machines with Discriminated Unions
Model state machines type-safely:
type IdleState = { status: 'idle' };
type LoadingState = { status: 'loading'; startedAt: Date };
type SuccessState = { status: 'success'; data: string };
type ErrorState = { status: 'error'; error: Error };
type State = IdleState | LoadingState | SuccessState | ErrorState;
function transition(state: State): State {
switch (state.status) {
case 'idle':
return { status: 'loading', startedAt: new Date() };
case 'loading':
// Can access startedAt here
return Math.random() > 0.5
? { status: 'success', data: 'result' }
: { status: 'error', error: new Error('Failed') };
case 'success':
case 'error':
return { status: 'idle' };
}
}
Intersection vs Extension
// Intersection - combines types
type A = { a: string };
type B = { b: number };
type C = A & B; // { a: string; b: number }
// Extension - interface inheritance
interface D extends A, B {
c: boolean;
}
// Intersection allows for more flexible composition
type Mixed = A & { c: boolean } & B;
Best Practices
- Use discriminated unions for mutually exclusive states
- Prefer union types for OR relationships
- Use intersection types for AND relationships
- Add exhaustiveness checks with never
- Use literal types in discriminants
Common Patterns
Optional Properties
type Config = {
required: string;
} & (
| { mode: 'development' }
| { mode: 'production'; apiKey: string }
);
Branded Types with Intersection
type Brand<T, B> = T & { __brand: B };
type UserId = Brand<string, 'UserId'>;
type Email = Brand<string, 'Email'>;
// UserId and Email are incompatible despite both being strings
Anti-Patterns
- Don’t use unions when discriminated unions are appropriate
- Avoid deep intersection nesting
- Don’t forget exhaustiveness checks
- Avoid conflicts in intersection types