typescript-type-patterns — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited typescript-type-patterns (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Control Flow Analysis (CFA) is the intelligence layer of the TypeScript compiler — it tracks the specific type of a variable at every point in the execution path. Mastering these patterns is essential for managing union types and building APIs that are impossible to misuse.
TypeScript types model sets of possible values. The type system is structural, not nominal — two types with the same shape are compatible. Every pattern below builds on this: narrowing refines the set, discriminated unions segment it, never represents the empty set, unknown represents the universal set.
Use narrowing to refine a broad type to a specific one through runtime control flow.
typeof — Primitive narrowingfunction formatId(id: string | number): string {
if (typeof id === 'string') {
return id.toUpperCase(); // id: string
}
return id.toFixed(0); // id: number
}instanceof — Class and error narrowingfunction handleError(err: unknown): string {
if (err instanceof Error) {
return err.message; // err: Error
}
return String(err);
}in operator — Object shape narrowingfunction processShape(shape: Circle | Square) {
if ('radius' in shape) {
return Math.PI * shape.radius ** 2; // shape: Circle
}
return shape.side ** 2; // shape: Square
}const users = [null, { name: 'Alice' }, null, { name: 'Bob' }];
const activeUsers = users.filter(Boolean); // (typeof users[0] | null)[]
// TS 5.5+: filter(Boolean) infers truthy type automaticallyDiscriminated unions are the most effective pattern for modeling complex application states, API responses, and messaging schemas. The discriminant is a shared literal property (kind, type, status) that distinguishes union members.
// Define the union with a shared literal discriminant
type ApiResult<T> =
| { kind: 'success'; data: T; statusCode: 200 }
| { kind: 'error'; message: string; statusCode: 400 | 404 | 500 }
| { kind: 'loading' };
// The compiler narrows based on the discriminant
function renderResult<T>(result: ApiResult<T>): string {
switch (result.kind) {
case 'success':
return JSON.stringify(result.data); // result: { kind: 'success'; data: T; ... }
case 'error':
return `Error ${result.statusCode}: ${result.message}`;
case 'loading':
return 'Loading...';
}
}Rules for discriminated unions:
kind or type property as the discriminantkind insteadnevernever represents a type with no possible values — the empty set. Use it to create compile-time guarantees that all union members are handled.
function assertNever(value: never): never {
throw new Error(`Unhandled case: ${JSON.stringify(value)}`);
}
function handleResult<T>(result: ApiResult<T>): string {
switch (result.kind) {
case 'success': return JSON.stringify(result.data);
case 'error': return result.message;
case 'loading': return 'Loading...';
default: return assertNever(result); // Compiler error if a case is missed
}
}If a new union member (e.g., { kind: 'cancelled' }) is added but not handled in the switch, the compiler flags the assertNever call as an error — the missing case is not assignable to never.
Type predicates (parameter is Type) encapsulate complex validation into reusable guard functions.
interface User { id: string; name: string; email: string; }
// Pre-TS-4.9 pattern: `in` narrowing required `as any` to access the property
// after the existence check. This is legacy — prefer TS 5.5 inferred predicates
// or Zod parsing for new code (see typescript-api-safety skill).
function isUser(obj: unknown): obj is User {
return (
typeof obj === 'object' &&
obj !== null &&
'id' in obj && typeof (obj as Record<string, unknown>).id === 'string' &&
'name' in obj && typeof (obj as Record<string, unknown>).name === 'string' &&
'email' in obj && typeof (obj as Record<string, unknown>).email === 'string'
);
}
// Note: use `as Record<string, unknown>` not `as any` — it's still an assertion
// but it preserves the constraint that values are unknown, not typed-but-unsafe.
// TypeScript now knows `data` is User after the guard
if (isUser(data)) {
console.log(data.email); // data: User
}Modern alternative (TS 5.5+): For validating external data, Zod schemas with.safeParse()are preferred over hand-written type predicates — they are safer, self-documenting, and free from manual assertion. Seetypescript-api-safetyskill.
TS 5.5 Inferred Type Predicates: The compiler can automatically infer that x => x !== null is a type guard, eliminating boilerplate in .filter() calls:
const values = [1, null, 2, null, 3];
const numbers = values.filter(x => x !== null); // number[] — inferred!unknown vs anyany. Accepts any value but forces narrowing before use.// any: no safety
function badProcess(data: any) {
return data.userId.trim(); // No error — crashes if userId is undefined
}
// unknown: forces proof
function safeProcess(data: unknown) {
if (isUser(data)) {
return data.email.trim(); // Narrowed to User — safe
}
throw new Error('Invalid user data');
}Rule: Any data from an external source (API, file, user input) starts as unknown. Parse it with Zod or a type predicate before use.
satisfies Operatorsatisfies validates that an expression matches a type without widening the most specific inferred type. Use it for configuration objects and lookup tables.
type Routes = Record<string, { path: string; auth: boolean }>;
// With type annotation — specific keys are lost (widened to string)
const routes: Routes = {
home: { path: '/', auth: false },
dashboard: { path: '/dashboard', auth: true },
};
routes.home; // Type: { path: string; auth: boolean }
// With satisfies — validation + literal preservation
const routes = {
home: { path: '/', auth: false },
dashboard: { path: '/dashboard', auth: true },
} satisfies Routes;
routes.home; // Type: { path: string; auth: boolean } AND routes.home is autocompleted
// routes.nonexistent; // Error! 'nonexistent' doesn't existUse `satisfies` for:
as const — Literal Preservation and Immutabilityas const recursively marks all properties readonly and prevents type widening.
// Without as const:
const config = { theme: 'dark', locale: 'en' };
// Type: { theme: string; locale: string } — widened!
// With as const:
const config = { theme: 'dark', locale: 'en' } as const;
// Type: { readonly theme: 'dark'; readonly locale: 'en' }
// Derive runtime list + compile-time union from single source of truth:
const STATUS_OPTIONS = ['pending', 'active', 'completed'] as const;
type Status = typeof STATUS_OPTIONS[number]; // 'pending' | 'active' | 'completed'
// Use STATUS_OPTIONS for iteration, Status for type checkingreadonly — Mutation PreventionSignal immutability intent and prevent side effects in shared state:
interface Config {
readonly apiUrl: string;
readonly timeout: number;
readonly retryPolicy: readonly ['immediate', 'exponential'];
}
// readonly is SHALLOW — object properties of readonly fields can still mutate
interface DeepConfig {
readonly nested: { value: string }; // nested.value can still be mutated!
}
// For deep immutability: use as const or Readonly<T> / ReadonlyArray<T>
type DeepReadonly<T> = { readonly [K in keyof T]: DeepReadonly<T[K]> };For detailed patterns and advanced techniques, consult:
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.