Advanced TypeScript Type Gymnastics
TypeScript's type system is Turing-complete. This means theoretically you can perform any computation at the type level. Understanding these advanced features lets you not only read type definitions in open-source libraries but also write precise type inference for your own code.
Core Tool: Conditional Types + infer
If you could only master one feature, it must be conditional types + infer. It's analogous to a ternary expression at the type level:
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T
type DeepReadonly<T> = T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T
type DeepRequired<T> = T extends object
? { [K in keyof T]-?: DeepRequired<T[K]> }
: T
type PickByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K]
}
type OmitByType<T, U> = {
[K in keyof T as T[K] extends U ? never : K]: T[K]
}
type PathOf<T, Prefix extends string = ''> = T extends object
? {
[K in keyof T & string]: K extends string
?
| `${Prefix}${K}`
| PathOf<T[K], `${Prefix}${K}.`>
: never
}[keyof T & string]
: never
type ValueAtPath<T, P extends string> = P extends `${infer K}.${infer Rest}`
? K extends keyof T
? ValueAtPath<T[K], Rest>
: never
: P extends keyof T
? T[P]
: never
type CamelCase<S extends string> = S extends `${infer First}_${infer Rest}`
? `${First}${Capitalize<CamelCase<Rest>>}`
: S
type SnakeCase<S extends string> = S extends `${infer First}${infer Rest}`
? First extends Uppercase<First>
? `_${Lowercase<First>}${SnakeCase<Rest>}`
: `${First}${SnakeCase<Rest>}`
: S
type Length<T extends readonly any[]> = T['length']
type First<T extends readonly any[]> = T extends [infer F, ...any] ? F : never
type Last<T extends readonly any[]> = T extends [...any, infer L] ? L : never
type TupleToUnion<T extends readonly any[]> = T[number]
type UnionToIntersection<U> = (
U extends any ? (k: U) => void : never
) extends (k: infer I) => void
? I
: never
type IsNever<T> = [T] extends [never] ? true : false
type IsUnion<T, U = T> = IsNever<T> extends true
? false
: T extends U
? [U] extends [T]
? false
: true
: false
type GetReturnType<F> = F extends (...args: any) => infer R ? R : never
type GetParameters<F> = F extends (...args: infer P) => any ? P : never
Common Utility Type Complexity Tiers
| Difficulty | Type | Core Technique | Practical Use |
|---|---|---|---|
| Beginner | Partial/Required/Readonly | Mapped types + ?/-?/readonly | Daily essential |
| Beginner | Pick/Omit/Record/Extract/Exclude | as clause + keyof + extends | Daily essential |
| Intermediate | ReturnType/Parameters/InstanceType | infer on function signatures | Type extraction |
| Intermediate | DeepPartial/DeepReadonly | Recursion + conditional types | Config objects |
| Intermediate | CamelCase/SnakeCase | Template literals + recursion | API transformation layers |
| Advanced | PathOf/ValueAtPath | Recursion + template literals | Type-safe lodash.get |
| Advanced | UnionToIntersection | Contravariant-position infer | Theoretical puzzles |
| Hell | ParseQueryString/Format | Multi-variable recursion | Type athletics |
The Secret of Distributive Conditional Types
When the left side of a conditional type is a naked type parameter and receives a union type, TypeScript automatically distributes:
type Wrap<T> = T extends any ? { value: T } : never
type X = Wrap<string | number>
// = { value: string } | { value: number }
// because string and number are substituted individually then unioned
This is the characteristic 90% of people don't understand but 90% of utility types use. Remember: to "process unions element-wise" use naked generics; to "process unions as a whole" wrap in tuples [T] extends [U].
Template Literal Types
Template literals introduced in TS 4.1 brought string processing to the type level:
type CSSVar<K extends string> = `--${K}`
type V = CSSVar<'primary-color'> // '--primary-color'
type Concat<A extends string, B extends string> = `${A}${B}`
Best Practices
- Write the JS runtime version first, then "translate" to the type version — drastically reduces mental effort
- Leverage the type-challenges repo — 20 intermediate-level questions will master most patterns
- Don't abuse in business code — excessively deep type gymnastics confuse coworkers; save deep dives for library code
- Write test cases for complex types with the
Expect<Equal<X, Y>>pattern to prevent regressions - Split sub-types past 5 levels of nesting — abstract and layer just like normal code
Summary
Type gymnastics is not showing off. Its essence is: catching errors that would otherwise surface at runtime and pushing them to compile time; letting the compiler auto-derive type declarations that would otherwise require hand-writing. Using it in the right places (library development, framework wrapping, general utilities) dramatically improves code quality and DX. In business code, however, remain restrained — enough is enough.