Skill — TypeScript Advanced Type Patterns
When this skill activates
Any task involving advanced TypeScript type system features: generics design,
conditional types, discriminated unions, mapped types, template literal types,
branded types, variance annotations, or complex type inference patterns.
Mandatory actions when this skill is active
Before writing any code
- Identify the type-level problem being solved. Ask: "What invalid states should
the type system make impossible?"
- Check the project's
tsconfig.json for strict: true. If not enabled, flag it
as a prerequisite for advanced type patterns to work correctly.
- Determine if the pattern needs to be exported for consumers (public API types
require more careful design than internal utility types).
During implementation
Generics
- Always add constraints to generic parameters:
<T extends BaseType> not bare <T>.
- Provide defaults when there is a sensible one:
<T extends Record<string, unknown> = Record<string, unknown>>.
- Place inference sites at the position where TypeScript can infer the type from usage:
// Good: T is inferred from the argument
function wrap<T>(value: T): Wrapper<T>
// Bad: T cannot be inferred, caller must specify
function wrap<T>(): Wrapper<T>
- Avoid "generic soup" (3+ type parameters). If needed, use a config object type:
type Config<TInput, TOutput, TError> = { ... }
function process<C extends Config<any, any, any>>(config: C): ...
Conditional Types
- Use
infer keyword to extract types from structures:type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never;
- Understand distributive behavior:
T extends U ? X : Y distributes over unions
when T is a naked type parameter. Wrap in [T] extends [U] to prevent distribution.
- Nest conditional types sparingly (max 3 levels). Extract intermediate types for clarity:
// Instead of deeply nested conditionals
type Step1<T> = T extends Array<infer U> ? U : T;
type Step2<T> = Step1<T> extends object ? keyof Step1<T> : never;
Discriminated Unions
- Every variant MUST have a literal-typed discriminant field (typically
type or kind):type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
- Implement exhaustive handling with
never checks:function assertNever(x: never): never {
throw new Error(`Unexpected: ${JSON.stringify(x)}`);
}
function area(shape: Shape): number {
switch (shape.kind) {
case "circle": return Math.PI * shape.radius ** 2;
case "rectangle": return shape.width * shape.height;
case "triangle": return 0.5 * shape.base * shape.height;
default: return assertNever(shape);
}
}
- Adding a new variant to the union automatically causes compile errors at every
switch/if that is missing the new case. This is the primary value.
Mapped Types
- Use key remapping (
as) for transforming keys:type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
- Use modifiers (
readonly, ?, -readonly, -?) deliberately:type Mutable<T> = { -readonly [K in keyof T]: T[K] };
type Required<T> = { [K in keyof T]-?: T[K] };
- Filter keys using
as with never:type OnlyStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
Template Literal Types
- Use for string manipulation at the type level:
type EventName<T extends string> = `on${Capitalize<T>}`;
type Route = `/${string}` | `/${string}/${string}`;
- Combine with mapped types for powerful API typing:
type CSSProperties = {
[K in keyof CSSStyleDeclaration as K extends string
? `--${K}` | K
: never]: string;
};
- Use intrinsic string types:
Uppercase, Lowercase, Capitalize, Uncapitalize.
Branded / Opaque Types
- Use for domain safety (preventing accidental misuse of primitive types):
declare const brand: unique symbol;
type Brand<T, B> = T & { readonly [brand]: B };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
// Now these are incompatible even though both are strings
function getUser(id: UserId): User { ... }
const orderId = "abc" as OrderId;
getUser(orderId); // Compile error!
- Provide constructor functions that validate at runtime:
function createUserId(raw: string): UserId {
if (!raw.startsWith("usr_")) throw new Error("Invalid UserId format");
return raw as UserId;
}
Variance Annotations
The satisfies Operator
- Use
satisfies to validate a value matches a type without widening:const palette = {
red: [255, 0, 0],
green: "#00ff00",
} satisfies Record<string, string | number[]>;
// palette.red is still number[] (not string | number[])
- Prefer
satisfies over as const + type annotation when you need both
type checking AND narrow inference.
Type Guards and Narrowing
- Prefer
is return type for custom type guards:function isCircle(shape: Shape): shape is { kind: "circle"; radius: number } {
return shape.kind === "circle";
}
- Use
asserts for assertion functions that throw:function assertDefined<T>(val: T | undefined): asserts val is T {
if (val === undefined) throw new Error("Expected defined value");
}
- Prefer
in operator narrowing for object type checks over typeof for complex objects.
After implementation
- Verify
npx tsc --noEmit passes with zero errors.
- Hover over inferred types in the IDE to confirm they resolve to expected shapes.
- Write at least one "negative test" — a
// @ts-expect-error comment proving the
type correctly rejects invalid usage.
- Check that type computation does not cause noticeable IDE lag (overly recursive
types can crash the language server).
Self-check before task completion
Before marking a task done when this skill was active:
1---2name: typescript-advanced3description: Skill — TypeScript Advanced Type Patterns4---56# Skill — TypeScript Advanced Type Patterns78## When this skill activates9Any task involving advanced TypeScript type system features: generics design,10conditional types, discriminated unions, mapped types, template literal types,11branded types, variance annotations, or complex type inference patterns.1213## Mandatory actions when this skill is active1415### Before writing any code161. Identify the type-level problem being solved. Ask: "What invalid states should17 the type system make impossible?"182. Check the project's `tsconfig.json` for `strict: true`. If not enabled, flag it19 as a prerequisite for advanced type patterns to work correctly.203. Determine if the pattern needs to be exported for consumers (public API types21 require more careful design than internal utility types).2223### During implementation2425#### Generics26- Always add constraints to generic parameters: `<T extends BaseType>` not bare `<T>`.27- Provide defaults when there is a sensible one: `<T extends Record<string, unknown> = Record<string, unknown>>`.28- Place inference sites at the position where TypeScript can infer the type from usage:29 ```typescript30 // Good: T is inferred from the argument31 function wrap<T>(value: T): Wrapper<T>32 // Bad: T cannot be inferred, caller must specify33 function wrap<T>(): Wrapper<T>34 ```35- Avoid "generic soup" (3+ type parameters). If needed, use a config object type:36 ```typescript37 type Config<TInput, TOutput, TError> = { ... }38 function process<C extends Config<any, any, any>>(config: C): ...39 ```4041#### Conditional Types42- Use `infer` keyword to extract types from structures:43 ```typescript44 type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;45 type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never;46 ```47- Understand distributive behavior: `T extends U ? X : Y` distributes over unions48 when T is a naked type parameter. Wrap in `[T] extends [U]` to prevent distribution.49- Nest conditional types sparingly (max 3 levels). Extract intermediate types for clarity:50 ```typescript51 // Instead of deeply nested conditionals52 type Step1<T> = T extends Array<infer U> ? U : T;53 type Step2<T> = Step1<T> extends object ? keyof Step1<T> : never;54 ```5556#### Discriminated Unions57- Every variant MUST have a literal-typed discriminant field (typically `type` or `kind`):58 ```typescript59 type Shape =60 | { kind: "circle"; radius: number }61 | { kind: "rectangle"; width: number; height: number }62 | { kind: "triangle"; base: number; height: number };63 ```64- Implement exhaustive handling with `never` checks:65 ```typescript66 function assertNever(x: never): never {67 throw new Error(`Unexpected: ${JSON.stringify(x)}`);68 }69 function area(shape: Shape): number {70 switch (shape.kind) {71 case "circle": return Math.PI * shape.radius ** 2;72 case "rectangle": return shape.width * shape.height;73 case "triangle": return 0.5 * shape.base * shape.height;74 default: return assertNever(shape);75 }76 }77 ```78- Adding a new variant to the union automatically causes compile errors at every79 switch/if that is missing the new case. This is the primary value.8081#### Mapped Types82- Use key remapping (`as`) for transforming keys:83 ```typescript84 type Getters<T> = {85 [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];86 };87 ```88- Use modifiers (`readonly`, `?`, `-readonly`, `-?`) deliberately:89 ```typescript90 type Mutable<T> = { -readonly [K in keyof T]: T[K] };91 type Required<T> = { [K in keyof T]-?: T[K] };92 ```93- Filter keys using `as` with `never`:94 ```typescript95 type OnlyStrings<T> = {96 [K in keyof T as T[K] extends string ? K : never]: T[K];97 };98 ```99100#### Template Literal Types101- Use for string manipulation at the type level:102 ```typescript103 type EventName<T extends string> = `on${Capitalize<T>}`;104 type Route = `/${string}` | `/${string}/${string}`;105 ```106- Combine with mapped types for powerful API typing:107 ```typescript108 type CSSProperties = {109 [K in keyof CSSStyleDeclaration as K extends string110 ? `--${K}` | K111 : never]: string;112 };113 ```114- Use intrinsic string types: `Uppercase`, `Lowercase`, `Capitalize`, `Uncapitalize`.115116#### Branded / Opaque Types117- Use for domain safety (preventing accidental misuse of primitive types):118 ```typescript119 declare const brand: unique symbol;120 type Brand<T, B> = T & { readonly [brand]: B };121122 type UserId = Brand<string, "UserId">;123 type OrderId = Brand<string, "OrderId">;124125 // Now these are incompatible even though both are strings126 function getUser(id: UserId): User { ... }127 const orderId = "abc" as OrderId;128 getUser(orderId); // Compile error!129 ```130- Provide constructor functions that validate at runtime:131 ```typescript132 function createUserId(raw: string): UserId {133 if (!raw.startsWith("usr_")) throw new Error("Invalid UserId format");134 return raw as UserId;135 }136 ```137138#### Variance Annotations139- Use `in` (contravariant) and `out` (covariant) on generic type parameters140 for explicit variance when the compiler cannot infer it:141 ```typescript142 type Consumer<in T> = (value: T) => void;143 type Producer<out T> = () => T;144 type Transformer<in I, out O> = (input: I) => O;145 ```146- Benefits: faster type checking, earlier error detection, documents intent.147- Only needed on interface/type alias declarations, not on function signatures.148149#### The `satisfies` Operator150- Use `satisfies` to validate a value matches a type without widening:151 ```typescript152 const palette = {153 red: [255, 0, 0],154 green: "#00ff00",155 } satisfies Record<string, string | number[]>;156 // palette.red is still number[] (not string | number[])157 ```158- Prefer `satisfies` over `as const` + type annotation when you need both159 type checking AND narrow inference.160161#### Type Guards and Narrowing162- Prefer `is` return type for custom type guards:163 ```typescript164 function isCircle(shape: Shape): shape is { kind: "circle"; radius: number } {165 return shape.kind === "circle";166 }167 ```168- Use `asserts` for assertion functions that throw:169 ```typescript170 function assertDefined<T>(val: T | undefined): asserts val is T {171 if (val === undefined) throw new Error("Expected defined value");172 }173 ```174- Prefer `in` operator narrowing for object type checks over `typeof` for complex objects.175176### After implementation1771. Verify `npx tsc --noEmit` passes with zero errors.1782. Hover over inferred types in the IDE to confirm they resolve to expected shapes.1793. Write at least one "negative test" — a `// @ts-expect-error` comment proving the180 type correctly rejects invalid usage.1814. Check that type computation does not cause noticeable IDE lag (overly recursive182 types can crash the language server).183184## Self-check before task completion185186Before marking a task done when this skill was active:187188- [ ] All generic type parameters have constraints (no bare `<T>` without `extends`).189- [ ] Discriminated unions have exhaustive switch/if with `never` fallback.190- [ ] No `any` types introduced (use `unknown` and narrow).191- [ ] Branded types have runtime validation constructors.192- [ ] Conditional types are no more than 3 levels deep.193- [ ] `@ts-expect-error` negative tests prove the types reject invalid input.194- [ ] `tsc --noEmit` passes cleanly.195- [ ] IDE responsiveness is acceptable (no type computation lag).