| name | typescript-strict-patterns |
| description | TypeScript strict mode patterns, branded types, discriminated unions, and type-safe architecture for MCP servers |
| license | MIT |
TypeScript Strict Patterns Skill
Context
This skill applies when:
- Writing TypeScript code with strict mode enabled
- Defining types and interfaces for European Parliament data
- Creating branded types for IDs to prevent mixing
- Implementing discriminated unions for type-safe variants
- Converting Zod schemas to TypeScript types
- Using utility types (Pick, Omit, Partial, Required)
- Handling nullable types safely
- Implementing generic type patterns
- Ensuring type safety across API boundaries
TypeScript strict mode is enabled in this project with strictNullChecks, noImplicitAny, and noUncheckedIndexedAccess. All code must comply with these strict rules.
Rules
- Never Use
any: Use unknown for truly dynamic types, then narrow with type guards
- Always Define Return Types: Explicitly type all function return values
- Use Branded Types for IDs: Prevent mixing different ID types (MEP_ID vs DocumentID)
- Leverage
z.infer<>: Derive TypeScript types from Zod schemas
- Handle Nulls Explicitly: Check for null/undefined before accessing properties
- Use Discriminated Unions: Type-safe variants with discriminator field
- Prefer
interface for Objects: Use type for unions, aliases, mapped types
- Use Utility Types: Leverage Pick, Omit, Partial, Required, Record
- Type All Parameters: Never rely on implicit parameter types
- Use
as const: For literal types and readonly values
Examples
✅ Good Pattern: Branded Types
import { z } from 'zod';
const MEP_ID = z.number().int().positive().brand<'MEP_ID'>();
type MEP_ID = z.infer<typeof MEP_ID>;
const DocumentIDSchema = z.string()
.regex(/^EP-\d{8}-\d{5}$/)
.brand<'DocumentID'>();
type DocumentID = z.infer<typeof DocumentIDSchema>;
function getMEP(id: MEP_ID): Promise<MEP> { }
function getDocument(id: DocumentID): Promise<Document> { }
const mepId: MEP_ID = MEP_ID.parse(12345);
const docId: DocumentID = DocumentIDSchema.parse('EP-20240101-00001');
await getMEP(mepId);
await getDocument(docId);
✅ Good Pattern: Discriminated Unions
interface WrittenQuestion {
type: 'written';
id: string;
questionText: string;
answerText?: string;
}
interface OralQuestion {
type: 'oral';
id: string;
questionText: string;
sessionDate: string;
}
interface PriorityQuestion {
type: 'priority';
id: string;
questionText: string;
deadline: string;
}
type ParliamentaryQuestion = WrittenQuestion | OralQuestion | PriorityQuestion;
function processQuestion(q: ParliamentaryQuestion): string {
switch (q.type) {
case 'written':
return `Written: ${q.answerText || 'pending'}`;
case 'oral':
return `Oral on ${q.sessionDate}`;
case 'priority':
return `Priority, deadline: ${q.deadline}`;
default:
const _exhaustive: never = q;
return _exhaustive;
}
}
✅ Good Pattern: Zod Schema to TypeScript Type
import { z } from 'zod';
const MEPSchema = z.object({
id: z.number().int().positive(),
fullName: z.string().min(1).max(255),
country: z.string().length(2),
partyGroup: z.string().min(1).max(50),
active: z.boolean(),
termStart: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
termEnd: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
committees: z.array(z.string()).default([]),
}).strict();
type MEP = z.infer<typeof MEPSchema>;
function validateMEP(data: unknown): MEP {
return MEPSchema.parse(data);
}
✅ Good Pattern: Utility Types
interface MEP {
id: number;
fullName: string;
country: string;
partyGroup: string;
email?: string;
phone?: string;
}
type MEPSummary = Pick<MEP, 'id' | 'fullName' | 'country'>;
type PublicMEP = Omit<MEP, 'email' | 'phone'>;
type MEPUpdate = Partial<MEP>;
type CompleteMEP = Required<MEP>;
type MEPMap = Record<number, MEP>;
type ImmutableMEP = Readonly<MEP>;
✅ Good Pattern: Null Safety
function getCommitteeName(mep: MEP, committeeCode?: string): string | null {
if (!committeeCode) {
return null;
}
const committee = mep.committees?.find(c => c.code === committeeCode);
if (!committee) {
return null;
}
return committee.name;
}
function getRequiredField(data: { field?: string }): string {
return data.field!;
}
function getRequiredFieldSafe(data: { field?: string }): string {
if (!data.field) {
throw new ValidationError('field is required');
}
return data.field;
}
✅ Good Pattern: Generic Types
interface APIResponse<T> {
data: T;
status: number;
cached: boolean;
latency: number;
}
async function fetchMEP(id: number): Promise<APIResponse<MEP>> {
const startTime = Date.now();
const mep = await getMEP(id);
return {
data: mep,
status: 200,
cached: false,
latency: Date.now() - startTime,
};
}
interface Cache<K, V> {
get(key: K): V | undefined;
set(key: K, value: V): void;
has(key: K): boolean;
delete(key: K): boolean;
clear(): void;
}
Anti-Patterns
❌ Bad: Using any
function bad(data: any): any {
return data.something.nested.value;
}
function good(data: unknown): string {
if (typeof data === 'object' && data !== null && 'value' in data) {
return String(data.value);
}
throw new Error('Invalid data structure');
}
❌ Bad: Implicit Return Types
function bad(id: number) {
return getMEP(id);
}
function good(id: number): Promise<MEP> {
return getMEP(id);
}
❌ Bad: Not Handling Nulls
function bad(mep: MEP): string {
return mep.email.toLowerCase();
}
function good(mep: MEP): string | null {
return mep.email?.toLowerCase() ?? null;
}
Type Guards
function isMEP(value: unknown): value is MEP {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
typeof value.id === 'number' &&
'fullName' in value &&
typeof value.fullName === 'string'
);
}
function processMEP(data: unknown): void {
if (isMEP(data)) {
console.log(data.fullName);
}
}
Mapped Types
type Nullable<T> = {
[P in keyof T]: T[P] | null;
};
type NullableMEP = Nullable<MEP>;
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
type MEPWithOptionalEmail = PartialBy<MEP, 'email' | 'phone'>;
Best Practices
- Explicit > Implicit: Always be explicit with types
- Narrow Types: Start with narrow types, widen if needed
- Use
const Assertions: For literal types (as const)
- Avoid Type Assertions: Use type guards instead of
as
- Document Complex Types: Add JSDoc for complex types
- Use Branded Types: For IDs and sensitive values
- Leverage Utility Types: Don't reinvent Pick, Omit, etc.
- Test Type Safety: Use TypeScript compiler in tests
ISMS Compliance
- SC-002: Type safety prevents runtime errors (input validation boundary)
- SC-001: Strong typing improves code quality and review depth
- VM-002: Typed boundaries reduce exploitable attack surface
Policy References
Primary:
Related: