typescript-anti-patterns — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited typescript-anti-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.
These patterns represent the most common ways TypeScript's type safety is subverted in production code — including patterns specifically introduced by AI coding assistants. Recognizing and eliminating them is essential for maintaining a trustworthy type system.
`any` is never acceptable in production code. It is a silent type system bypass.
// ❌ any — disables all type checking on this variable
function processData(data: any) {
return data.userId.trim(); // No error — crashes if userId missing
}
// ❌ Implicit any in function parameters
function log(message) { // noImplicitAny catches this
console.log(message);
}
// ✅ Use unknown — forces proof before use
function processData(data: unknown) {
if (!isUser(data)) throw new Error('Invalid user data');
return data.email.trim(); // Narrowed — safe
}When AI generates `any`: Replace with unknown + type guard or Zod parsing. Never accept // @ts-ignore or // @ts-expect-error without a specific documented reason.
as Type)Type assertions at API boundaries or around unknown data are red flags.
// ❌ Classic assertion smell — lies to the compiler
const user = response.json() as User;
user.email.trim(); // Compiles, but crashes if API schema changed
// ❌ Double assertion — bypasses type checking entirely
const hack = value as unknown as SpecificType;
// ❌ Object literal assertion — excess property checks bypassed
const config = { url: '/api', timeout: 5000, typoKey: true } as Config;
// ✅ Correct — parse and prove at the boundary
const result = UserSchema.safeParse(await response.json());
if (!result.success) return sendError(400, result.error.flatten());
const user = result.data; // Typed and verifiedAcceptable uses of `as`:
as const — literal preservation and immutability (encouraged)!)The ! operator is a lie: it tells the compiler "trust me, this is not null/undefined."
// ❌ Fragile — breaks when code is moved or refactored
const name = user!.profile!.name!;
// ❌ AI-generated: spray ! to silence null check errors
const element = document.querySelector('.btn')!.addEventListener('click', handler);
// ✅ Explicit null checks
const profile = user?.profile;
if (!profile?.name) return;
const name = profile.name; // Narrowed — safe
// ✅ Discriminated union eliminates the need for !
type LoggedInUser = { status: 'authenticated'; profile: Profile };
if (user.status === 'authenticated') {
const name = user.profile.name; // Compiler proves profile exists
}Empty catches hide bugs. They make debugging nearly impossible.
// ❌ Silent swallow
try {
await processOrder(order);
} catch { } // Bug disappears here
// ❌ Logging is not handling
try {
await processOrder(order);
} catch (err) {
console.error(err); // Logged in dev, missing in prod log aggregation
}
// ✅ Actually handle or re-throw with context
try {
await processOrder(order);
} catch (err) {
if (err instanceof ValidationError) {
return { ok: false, error: 'VALIDATION_FAILED', details: err.fields };
}
throw new Error(`Order processing failed for order ${order.id}`, { cause: err });
}Annotating with a broad type loses the specific type information.
// ❌ Type annotation widens literals
const config: Record<string, string> = {
theme: 'dark',
locale: 'en-US',
};
config.theme; // Type: string (lost 'dark')
config.nonexistent; // No error!
// ✅ satisfies validates without widening
const config = {
theme: 'dark',
locale: 'en-US',
} satisfies Record<string, string>;
config.theme; // Type: 'dark' (preserved)
config.nonexistent; // Error! (key validation)
// ✅ as const for complete immutability
const THEMES = ['dark', 'light', 'system'] as const;
type Theme = typeof THEMES[number]; // 'dark' | 'light' | 'system'Numeric enums create runtime IIFE objects, allow reverse mapping, and accept any number.
// ❌ Numeric enum — runtime overhead, numeric assignment allowed
enum Status {
Pending, // = 0
Active, // = 1
Inactive, // = 2
}
const s: Status = 999; // No error! Any number is assignable
// ❌ Regular string enum — generates runtime object, breaks tree-shaking
enum Role {
Admin = 'ADMIN',
User = 'USER',
}
// ✅ String literal union — zero runtime cost, no ambiguity
type Status = 'pending' | 'active' | 'inactive';
// ✅ as const for runtime array + type (when iteration needed)
const STATUS_OPTIONS = ['pending', 'active', 'inactive'] as const;
type Status = typeof STATUS_OPTIONS[number];
// ✅ const enum — inlined at compile time (but avoid with bundlers)
const enum Direction { Up, Down, Left, Right }
// Compiles to: 0, 1, 2, 3 at use sites — no runtime object
// Warning: requires --isolatedModules: false, breaks with esbuild/swcOverly abstract generics reduce readability and increase compile times.
// ❌ Generic soup — unreadable, confusing, poor DX
type DeepNested<T, K extends keyof T, V extends T[K], R extends Record<K, V>> =
R extends Record<infer KK, infer VV> ? VV extends string ? KK : never : never;
// ❌ Too many generic parameters
function transform<T, K extends keyof T, V extends T[K], R>(
obj: T, key: K, transform: (v: V) => R, fallback: R
): Record<K, R> { ... }
// ✅ If you can't describe the generic in one sentence, simplify
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}Rule: If a function has more than 3 generic parameters, refactor. If you cannot describe what T represents in one sentence, the abstraction is too complex.
Exported mutable objects can be modified by any importer — a major source of hidden state bugs.
// ❌ Exported mutable array — any importer can push/splice
export const adminUsers: User[] = [];
// Elsewhere: adminUsers.push(maliciousUser); // Undetected
// ❌ Exported mutable config
export const config = {
apiUrl: 'https://api.example.com',
retries: 3,
};
// Elsewhere: config.retries = 0; // Silently breaks retry logic
// ✅ Export readonly types
export const adminUsers: ReadonlyArray<User> = Object.freeze([]);
export const config = Object.freeze({
apiUrl: 'https://api.example.com',
retries: 3,
} as const);
// ✅ Expose mutation through controlled functions
let users: User[] = [];
export const userStore = {
get: (): ReadonlyArray<User> => users,
add: (user: User): void => { users = [...users, user]; },
};AI coding assistants consistently introduce these patterns — always review AI-generated TypeScript:
| Pattern | What AI Does | Correct Pattern | |
|---|---|---|---|
any spray | Uses any for "complex" types | unknown + type guard | |
! spray | Adds ! to silence null warnings | Explicit null check or discriminated union | |
.parse() instead of .safeParse() | Throws on invalid input | .safeParse() + check .success | |
Empty catch {} | Swallows errors silently | Classify and handle | |
| Manual type-only imports | Uses import instead of import type | import type | |
Missing override keyword | Shadows base class methods | Add override | |
| Broad type annotations | const x: object = { key: 'value' } | satisfies or as const | |
String JSON.parse() | JSON.parse(str) as MyType | MySchema.safeParse(JSON.parse(str)) | |
| Enum over union | enum Status { Active, Inactive } | `type Status = 'active' | 'inactive'` |
To prevent anti-patterns from persisting in the codebase:
strict: true + noUncheckedIndexedAccess + exactOptionalPropertyTypesno-explicit-any, no-non-null-assertion, consistent-type-assertionstsc --noEmit && eslint . --max-warnings 0 before reviewas, !, any, and empty catch blocksFor the complete anti-patterns audit guide and AI codegen guardrails, consult:
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.