Advanced TypeScript: Conditional Types & Template Literal Types
- Conditional types are the type-level if/else — T extends U ? X : Y selects between types based on structural compatibility
- Distribution is automatic when T is a bare type parameter — wrap in a tuple [T] to suppress it
- infer is pattern matching for types — it binds a type variable to the matched portion of a structure
- Conditional types select between two types based on a condition: T extends U ? X : Y
- Template literal types construct string types from unions and interpolations — they are the type-level equivalent of string templates
- infer extracts types from within extends clauses — ReturnType
is built on infer - Distributive conditional types iterate over union members automatically — one of TypeScript's least understood behaviors
- Real-world use case: type-safe event emitters, API route handlers, and CSS-in-JS libraries rely on these features daily
- Biggest mistake: confusing extends in conditional types (structural compatibility) with extends in classes (inheritance)
Type resolves to never unexpectedly
npx tsc --noEmit --explainFiles 2>&1 | grep -i 'never'grep -rn 'extends.*?' src/ --include='*.ts' | grep -v '\[' | head -10Template literal type collapses to string
npx tsc --noEmit 2>&1 | grep 'not assignable to type' | head -5cat src/types.ts | grep -A 2 '\${' | head -20Recursive type hits depth limit
npx tsc --noEmit 2>&1 | grep 'excessively deep'grep -rn 'T extends' src/types.ts | grep -v 'infer' | head -10Mapped type key remapping fails
npx tsc --noEmit 2>&1 | grep 'Type .* is not assignable' | head -5cat src/types.ts | grep -B 2 -A 2 '\[K in' | head -20Production Incident
type IsNever<T> = T extends never ? true : false. It was called with a union produced by Filter<Keys, ValidKeys>. Because T was a bare type parameter, TypeScript distributed the check over each union member. A union never distributes to zero constituents, so T extends never never matched and always resolved to false. Downstream code treated 'not never' as 'has values' and propagated a union that should have been discarded, eventually collapsing to never.type IsNever<T> = [T] extends [never] ? true : false. This forced TypeScript to evaluate the union as a whole. With the fix, empty extractions correctly resolved to true, allowing the upstream utility to return never for invalid keys, and API response types inferred properly again.Production Debug GuideDiagnose type inference failures, distribution issues, and template literal constraints
Conditional types and template literal types are TypeScript's most powerful type-level features. They enable patterns that eliminate boilerplate, enforce contracts at compile time, and make libraries like tRPC, Zod, and styled-components possible.
These features shipped in TypeScript 4.1 (template literals) and were strengthened in 4.5+ for recursive conditionals — you'll need TS 4.1 or later to use the patterns in this article.
Most engineers stop at generics and interfaces. The gap between 'knows TypeScript' and 'writes library-grade types' is conditional types, infer, and template literals. These features let you derive types from runtime structures, extract return types from functions, and construct string patterns that the compiler validates.
The misconception: these features are academic. They are not. Every major TypeScript codebase uses conditional types — through ReturnType, Exclude, Extract, Parameters, and Awaited. Understanding the primitives lets you build your own type utilities instead of relying on the standard library alone.
Conditional Types: The Type-Level If/Else
A conditional type selects one of two types based on a condition. The syntax mirrors a ternary expression: T extends U ? X : Y. If T is assignable to U, the type resolves to X. Otherwise, it resolves to Y.
The extends keyword in conditional types means structural compatibility — not class inheritance. T extends U is true when T has all the properties and methods that U requires. This is the same check TypeScript uses for function argument assignment.
The critical behavior: when T is a union type, conditional types distribute over each member. A | B extends U ? X : Y becomes (A extends U ? X : Y) | (B extends U ? X : Y). This is powerful but dangerous — it is the source of most conditional type bugs in production codebases.
// ============================================ // Conditional Types — Core Patterns // ============================================ // ---- Basic conditional type ---- // T extends U ? X : Y type IsString<T> = T extends string ? 'yes' : 'no' type A = IsString<string> // 'yes' type B = IsString<number> // 'no' type C = IsString<'hello'> // 'yes' — string literal extends string // ---- Distributive behavior: the source of most bugs ---- // When T is a bare type parameter, conditional types distribute over unions type ToArray<T> = T extends unknown ? T[] : never type D = ToArray<string | number> // Resolves to: string[] | number[] // NOT: (string | number)[] // TypeScript distributes: (string extends unknown ? string[] : never) | // (number extends unknown ? number[] : never) // ---- Suppressing distribution with tuples ---- // Wrap T in a tuple to prevent distribution type ToArrayNonDist<T> = [T] extends [unknown] ? T[] : never type E = ToArrayNonDist<string | number> // Resolves to: (string | number)[] // Distribution suppressed — the entire union is treated as one type // ---- Built on conditional types: Exclude, Extract, NonNullable ---- // These are in TypeScript's standard library — understanding them requires // understanding distributive conditional types type Exclude<T, U> = T extends U ? never : T type F = Exclude<'a' | 'b' | 'c', 'a'> // Distributes: ('a' extends 'a' ? never : 'a') | // ('b' extends 'a' ? never : 'b') | // ('c' extends 'a' ? never : 'c') // Resolves to: 'b' | 'c' // ---- Practical: Filter union members by shape ---- type FilterByShape<T, U> = T extends U ? T : never interface HasId { id: string } type User = { id: string; name: string } type Product = { id: string; price: number } type Error = { message: string } type Entities = FilterByShape<User | Product | Error, HasId> // Resolves to: User | Product // Error is excluded because it lacks 'id' // ---- Practical: Conditional return type based on input ---- // The conditional type describes the relationship; overloads implement it type ProcessReturn<T> = T extends string ? string[] : number function process(value: string): string[] function process(value: number): number function process(value: string | number): string[] | number { return typeof value === 'string' ? value.split('') : value * 2 } const resultA = process('hello') // string[] const resultB = process(42) // number // TypeScript infers ProcessReturn<T> via overloads
- T extends U ? X : Y distributes — each union member is checked independently
- [T] extends [U] ? X : Y does not distribute — the entire union is checked as one type
- Distribution is automatic when T is a bare type parameter — wrapping in a tuple suppresses it
- Exclude, Extract, and NonNullable all rely on distributive behavior
- Most production bugs come from unexpected distribution — always test with union inputs
The infer Keyword: Extracting Types from Structures
infer lets you extract a type from within an extends clause. It introduces a type variable that TypeScript binds to the matched portion of the type. The syntax: T extends SomePattern<infer X> ? X : fallback.
infer only works inside the extends clause of a conditional type. You cannot use it in interfaces, type aliases, or function signatures directly. It is a pattern-matching tool — the pattern must match the input type's structure for infer to bind.
The most common use: extracting return types, parameter types, and resolved promise types. ReturnType<T>, Parameters<T>, and Awaited<T> are all built on infer. Understanding infer lets you build custom extractors for any structure.
// ============================================ // infer Keyword — Type Extraction Patterns // ============================================ // ---- Basic infer: extract return type ---- // This is how ReturnType<T> works in the standard library type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never function getUser() { return { id: '1', name: 'Alice', role: 'admin' as const } } type UserReturn = MyReturnType<typeof getUser> // Resolves to: { id: string; name: string; role: 'admin' } // ---- Extract parameter types ---- type MyParameters<T> = T extends (...args: infer P) => any ? P : never function createUser(name: string, age: number, email: string) { return { name, age, email } } type CreateUserParams = MyParameters<typeof createUser> // Resolves to: [name: string, age: number, email: string] // ---- Extract promise resolution type ---- type UnwrapPromise<T> = T extends Promise<infer U> ? U : T async function fetchUser() { const response = await fetch('/api/user') return response.json() as Promise<{ id: string; name: string }> } type FetchUserType = UnwrapPromise<ReturnType<typeof fetchUser>> // Resolves to: { id: string; name: string } // ---- Multiple infer positions ---- // Extract both input and output of a function type FunctionSignature<T> = T extends ( ...args: infer P ) => infer R ? { params: P; returnValue: R } : never type Sig = FunctionSignature<typeof createUser> // Resolves to: { // params: [name: string, age: number, email: string] // returnValue: { name: string; age: number; email: string } // } // ---- Infer in recursive types ---- // Extract nested types from deeply structured objects type DeepValue< T, Path extends string > = Path extends `${infer Key}.${infer Rest}` ? Key extends keyof T ? DeepValue<T[Key], Rest> : never : Path extends keyof T ? T[Path] : never interface AppConfig { database: { host: string port: number credentials: { username: string password: string } } cache: { ttl: number maxSize: number } } type DbHost = DeepValue<AppConfig, 'database.host'> // Resolves to: string type DbPassword = DeepValue<AppConfig, 'database.credentials.password'> // Resolves to: string type CacheTtl = DeepValue<AppConfig, 'cache.ttl'> // Resolves to: number // ---- Practical: type-safe event handler extraction ---- type EventHandler<T, EventName extends string> = T extends { [K in EventName]: (...args: infer P) => any } ? P : never interface AppEvents { click: (x: number, y: number) => void submit: (data: FormData) => void error: (code: number, message: string) => void } type ClickArgs = EventHandler<AppEvents, 'click'> // Resolves to: [x: number, y: number] type ErrorArgs = EventHandler<AppEvents, 'error'> // Resolves to: [code: number, message: string]
- infer only works inside extends clauses — you cannot use it in interfaces or standalone type aliases
- The pattern must structurally match the input type — if it does not match, infer produces never
- Multiple infer positions extract multiple parts: (args: infer P) => infer R binds both P and R
- infer works inside template literals too:
${infer Head}.${infer Tail}— that's how you split strings at the type level - ReturnType, Parameters, Awaited are all built on infer — you can build custom extractors the same way
Template Literal Types: String Construction at the Type Level
Template literal types build string types by interpolating other types into a string template. The syntax mirrors template literals in JavaScript: prefix-${Type}-suffix. TypeScript computes the resulting string type at compile time.
When the interpolated type is a union, the template literal produces a union of all combinations. get-${'user' | 'post'} resolves to 'get-user' | 'get-post'. This combinatorial explosion is powerful but can produce very large union types — TypeScript has a limit of 100,000 members before it collapses to string.
Template literal types pair with mapped types and conditional types to create type-safe APIs. The pattern: define a template, constrain inputs to match, and let TypeScript enforce the contract. This is how tRPC infers procedure names and how styled-components type CSS properties.
// ============================================ // Template Literal Types — String Construction Patterns // ============================================ // ---- Basic template literal type ---- type EventName = `on${Capitalize<'click' | 'focus' | 'blur'>}` // Resolves to: 'onClick' | 'onFocus' | 'onBlur' // ---- Combinatorial expansion ---- // Union × Union = all combinations type HTTPMethod = 'get' | 'post' | 'put' | 'delete' type Resource = 'user' | 'post' | 'comment' type APIRoute = `/${Resource}` | `/${Resource}/${string}` // Resolves to: '/user' | '/user/${string}' | '/post' | ... // Combinatorial growth example type A = 'a' | 'b' | 'c' // 3 members type AB = `${A}-${A}` // 9 members (3×3) type ABC = `${AB}-${AB}` // 81 members (9×9) — grows exponentially // ---- Practical: type-safe CSS property access ---- type CSSProperty = `--${string}` type CSSValue = { [K in CSSProperty]?: string } const theme: CSSValue = { '--color-primary': '#3b82f6', '--color-background': '#ffffff', '--spacing-unit': '8px', } // TypeScript validates that all keys start with '--' // const bad: CSSValue = { 'color': 'red' } // Error // ---- Practical: type-safe API endpoint builder ---- 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'> // Resolves to: 'userId' | 'postId' // Build a type-safe route function function createRoute<T extends string>( template: T, params: Record<ExtractParams<T>, string> ): string { let result = template as string for (const [key, value] of Object.entries(params)) { result = result.replace(`:${key}`, value) } return result } const url = createRoute('/users/:userId/posts/:postId', { userId: 'u-123', postId: 'p-456', }) // url: string — '/users/u-123/posts/p-456' // TypeScript catches missing or extra params: // createRoute('/users/:userId', { wrong: 'key' }) // Error // createRoute('/users/:userId', {}) // Error: missing userId // ---- Mapped types with template literal keys ---- // Remap object keys using template literals type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] } type User = { id: string name: string email: string } type UserGetters = Getters<User> // Resolves to: { // getId: () => string // getName: () => string // getEmail: () => string // } type Setters<T> = { [K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void } type UserSetters = Setters<User> // Resolves to: { // setId: (value: string) => void // setName: (value: string) => void // setEmail: (value: string) => void // } // ---- Combining template literals with conditional types ---- type ExtractPrefix<T extends string> = T extends `${infer Prefix}:${string}` ? Prefix : T type Prefix = ExtractPrefix<'content-type:application/json'> // Resolves to: 'content-type' type ExtractSuffix<T extends string> = T extends `${string}:${infer Suffix}` ? Suffix : T type Suffix = ExtractSuffix<'content-type:application/json'> // Resolves to: 'application/json'
prefix-${Union}produces a union of all combinations — 10 × 10 = 100 members- TypeScript collapses to string above 100,000 members — monitor union sizes in mapped types
- Capitalize, Uppercase, Lowercase, Uncapitalize are built-in utility types for template literals
- Mapped type key remapping (as
prefix-${K}) lets you transform object keys at the type level - Template literals + infer enable recursive string parsing — extract segments from dot-separated or slash-separated paths
prefix-${Union} produces all combinations.get${Capitalize<K>} generates getter signatures.Pattern: Type-Safe Event Emitter
Event emitters are a natural fit for conditional types and template literals. The goal: enforce that event handlers match their event's payload type, and that event names are constrained to a known set. Without these type features, event emitters accept any string and any handler — runtime errors are the only safety net.
The pattern combines mapped types (for the handler registry), template literals (for event name construction), and conditional types (for extracting handler signatures). Libraries like Node.js EventEmitter, Socket.io, and browser EventTargets all benefit from this pattern.
// ============================================ // Type-Safe Event Emitter — Conditional + Template Literal Pattern // ============================================ // ---- Event map: defines events and their payload types ---- type EventMap = { 'user:login': { userId: string; timestamp: number } 'user:logout': { userId: string } 'order:created': { orderId: string; total: number } 'order:shipped': { orderId: string; trackingNumber: string } 'error': { code: number; message: string } } // ---- Type-safe event emitter ---- class TypedEmitter<TEvents extends Record<string, any>> { private listeners = new Map<string, Set<Function>>() on<K extends keyof TEvents>( event: K, handler: (payload: TEvents[K]) => void ): () => void { if (!this.listeners.has(event as string)) { this.listeners.set(event as string, new Set()) } this.listeners.get(event as string)!.add(handler) // Return unsubscribe function return () => { this.listeners.get(event as string)?.delete(handler) } } emit<K extends keyof TEvents>( event: K, payload: TEvents[K] ): void { this.listeners.get(event as string)?.forEach((handler) => { handler(payload) }) } off<K extends keyof TEvents>( event: K, handler: (payload: TEvents[K]) => void ): void { this.listeners.get(event as string)?.delete(handler) } } // ---- Usage: full type safety ---- const emitter = new TypedEmitter<EventMap>() // Correct: handler payload type matches event emitter.on('user:login', (data) => { console.log(data.userId) // string — typed console.log(data.timestamp) // number — typed }) // Error: handler receives wrong payload type // emitter.on('user:login', (data: { wrong: boolean }) => {}) // Error: unknown event name // emitter.on('unknown:event', () => {}) // Emit with correct payload emitter.emit('order:created', { orderId: 'ord-123', total: 99.99, }) // Error: missing payload property // emitter.emit('order:created', { orderId: 'ord-123' }) // ---- Wildcard event handler with conditional types ---- type AnyEvent<T> = keyof T type AnyPayload<T> = T[keyof T] // Handler that receives all events with discriminated payload type WildcardHandler<TEvents extends Record<string, any>> = < K extends keyof TEvents >( event: K, payload: TEvents[K] ) => void // Register wildcard handler // Note: simplified for type demonstration — a real implementation iterates // over known event keys from the EventMap and subscribes individually function onAny<TEvents extends Record<string, any>>( emitter: TypedEmitter<TEvents>, handler: WildcardHandler<TEvents> ): () => void { // production code would register handler for each key in TEvents return () => { // unsubscribe logic } } // ---- Event name builder using template literals ---- type Domain = 'user' | 'order' | 'payment' type Action = 'created' | 'updated' | 'deleted' type BuildEventName<D extends string, A extends string> = `${D}:${A}` type AllEvents = BuildEventName<Domain, Action> // Resolves to: 'user:created' | 'user:updated' | 'user:deleted' | // 'order:created' | 'order:updated' | 'order:deleted' | // 'payment:created' | 'payment:updated' | 'payment:deleted'
- Event maps define the contract — one type maps event names to payload types
- Mapped types enforce that handlers receive the correct payload for each event
- Template literals build event name unions from domain + action combinations
- Unsubscribe functions are returned from on() — prevents memory leaks from orphaned listeners
- Wildcard handlers use conditional types to accept any event with a discriminated payload
Pattern: Recursive Type for Deep Path Access
Deep path access — reading nested object properties via dot-separated strings — is common in form libraries, ORM query builders, and configuration systems. Without advanced types, these systems accept any string and return unknown. With recursive conditional types and template literals, TypeScript validates paths at compile time and infers the value type.
The pattern: split the path string on '.' using template literal + infer, recurse into the object type using the first segment as a key, and continue until the path is exhausted. The base case: when the path has no dots, look up the key directly.
This pattern powers libraries like lodash's get/set functions with type safety, react-hook-form's register function, and Prisma's select/include query builders.
// ============================================ // Recursive Deep Path Access — Template Literal + Conditional Pattern // ============================================ // ---- Deep value extractor ---- // Splits dot-separated path and recurses into object type type DeepValue< T, Path extends string > = Path extends `${infer Key}.${infer Rest}` ? Key extends keyof T ? DeepValue<T[Key], Rest> : never : Path extends keyof T ? T[Path] : never // ---- Deep path generator ---- // Produces all valid dot-separated paths for an object type type DeepPaths<T, Prefix extends string = ''> = T extends object ? T extends any[] | Date | Function ? never : { [K in keyof T & string]: T[K] extends object ? T[K] extends any[] | Date | Function ? `${Prefix}${K}` : `${Prefix}${K}` | DeepPaths<T[K], `${Prefix}${K}.`> : `${Prefix}${K}` }[keyof T & string] : never // ---- Usage ---- interface AppConfig { server: { host: string port: number ssl: { enabled: boolean cert: string key: string } } database: { host: string port: number name: string pool: { min: number max: number } } cache: { ttl: number driver: 'memory' | 'redis' } } // All valid paths type ConfigPaths = DeepPaths<AppConfig> // Resolves to: // 'server' | 'server.host' | 'server.port' | 'server.ssl' | // 'server.ssl.enabled' | 'server.ssl.cert' | 'server.ssl.key' | // 'database' | 'database.host' | 'database.port' | 'database.name' | // 'database.pool' | 'database.pool.min' | 'database.pool.max' | // 'cache' | 'cache.ttl' | 'cache.driver' // Value type from path type SslEnabled = DeepValue<AppConfig, 'server.ssl.enabled'> // Resolves to: boolean type PoolMax = DeepValue<AppConfig, 'database.pool.max'> // Resolves to: number type CacheDriver = DeepValue<AppConfig, 'cache.driver'> // Resolves to: 'memory' | 'redis' // ---- Type-safe get function ---- function deepGet<T extends object, P extends DeepPaths<T>>( obj: T, path: P ): DeepValue<T, P> { const keys = (path as string).split('.') let result: any = obj for (const key of keys) { result = result[key] } return result } const config: AppConfig = { server: { host: 'localhost', port: 3000, ssl: { enabled: true, cert: '/cert.pem', key: '/key.pem' } }, database: { host: 'db.local', port: 5432, name: 'app', pool: { min: 2, max: 10 } }, cache: { ttl: 3600, driver: 'redis' }, } const sslEnabled = deepGet(config, 'server.ssl.enabled') // boolean const poolMax = deepGet(config, 'database.pool.max') // number const driver = deepGet(config, 'cache.driver') // 'memory' | 'redis' // Error: invalid path // deepGet(config, 'server.ssl.nonexistent') // Type error // deepGet(config, 'invalid.path') // Type error // ---- Type-safe set function ---- function deepSet<T extends object, P extends DeepPaths<T>>( obj: T, path: P, value: DeepValue<T, P> ): void { const keys = (path as string).split('.') let target: any = obj for (let i = 0; i < keys.length - 1; i++) { target = target[keys[i]] } target[keys[keys.length - 1]] = value } deepSet(config, 'server.port', 8080) // OK — number deepSet(config, 'cache.driver', 'memory') // OK — 'memory' | 'redis' // Error: wrong value type // deepSet(config, 'server.port', 'eighty') // Type error — string not assignable to number
- Base case: when the path has no dots, look up the key directly — this stops recursion
- TypeScript has a default depth limit of 50 instantiations — deeply nested paths may hit this
- DeepPaths<T> generates all valid paths — use it to constrain the path parameter
- DeepValue<T, P> extracts the value type — use it as the return type or value constraint
- This pattern powers react-hook-form's register, Prisma's select, and lodash's typed get/set
| Feature | Conditional Types | Template Literal Types |
|---|---|---|
| Primary use | Select between types based on a condition | Construct string types from unions and interpolations |
| Syntax | T extends U ? X : Y | prefix-${Type}-suffix |
| Distribution | Distributes over unions by default | Expands unions combinatorially |
| Key keyword | extends (structural compatibility) | infer (pattern extraction within extends) |
| Built-in examples | Exclude, Extract, NonNullable, ReturnType | Capitalize, Uppercase, Lowercase, Uncapitalize |
| Composability | Used inside template literals via infer | Used as the extends clause target in conditionals |
| Performance risk | Recursive types can hit depth limit | Large unions collapse to string above 100K members |
| Common bug | Unexpected distribution over unions | Collapsed to string when interpolated type is too wide |
🎯 Key Takeaways
- Conditional types are the type-level if/else — T extends U ? X : Y selects between types based on structural compatibility
- Distribution is automatic when T is a bare type parameter — wrap in a tuple [T] to suppress it
- infer is pattern matching for types — it binds a type variable to the matched portion of a structure
- Template literal types construct string types from unions — combinatorial expansion can exceed 100K members
- Recursive conditional types need a base case — without one, TypeScript hits its instantiation depth limit
- These features power tRPC, Zod, Prisma, react-hook-form, and styled-components — they are not academic
⚠ Common Mistakes to Avoid
Interview Questions on This Topic
- QWhat is the difference between distributive and non-distributive conditional types in TypeScript?Mid-levelReveal
- QHow does the infer keyword work in conditional types, and what are its limitations?Mid-levelReveal
- QWhat happens when you interpolate a union type into a template literal type?SeniorReveal
- QHow would you implement a type-safe deep path accessor like lodash's get() using conditional types and template literals?SeniorReveal
- QWhat is the relationship between conditional types and TypeScript's built-in utility types?Mid-levelReveal
Frequently Asked Questions
When should I use conditional types versus simple type narrowing?
Use conditional types when you need the type system to compute a different type based on another type's structure — before any runtime value exists. Type narrowing (typeof, instanceof, discriminated unions) operates on values at runtime. Conditional types operate on types at compile time. Example: ReturnType<T> computes the return type of a function type without calling the function. You cannot achieve this with narrowing because narrowing requires a runtime value.
Can template literal types be used for runtime validation?
No. Template literal types are purely compile-time constructs — they constrain and compute string types but do not generate runtime validation logic. For runtime validation, use a library like Zod that mirrors your type constraints as runtime checks. The pattern: define the type with template literals, then write a Zod schema that enforces the same pattern with regex or string parsing at runtime.
How do I debug a conditional type that produces unexpected results?
Use the TypeScript Playground's 'Evaluate' panel to step through type resolution. Hover over the type alias to see the resolved type. For distribution issues, test with a single type first, then a union — if the behavior changes, distribution is the cause. For infer issues, verify the pattern matches the input type's structure exactly. The utility type helper trick: type Debug<T> = T extends any ? { resolved: T } : never — wrapping in a mapped type forces TypeScript to show intermediate results.
What is the performance impact of complex conditional types on TypeScript compilation?
Complex conditional types — especially recursive ones — increase compilation time. TypeScript caches type evaluations, but deeply recursive types (50+ levels) hit the instantiation depth limit and produce errors. Large template literal unions (approaching 100K members) also slow the compiler. Mitigation: add base cases to recursive types, constrain union sizes before interpolating into template literals, and avoid unnecessary type-level computation that could be expressed as simpler types.
How do conditional types relate to covariance and contravariance in TypeScript?
Conditional types are covariant in their true and false branches — the resolved type preserves the variance of the input. However, the extends check itself involves both covariance and contravariance depending on position. Function parameter types are contravariant, so T extends (arg: U) => any checks whether T's parameter type is a supertype of U. This matters when writing conditional types that operate on function types — the variance direction determines which types match the extends clause.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.