// BAD: any everywherefunction process(data: any): any { ... }
// GOOD: use proper typesfunction process(data: unknown): Result { ... }
example
// BAD — any silently disables all type safetyfunction parseConfig(raw: any) {
return raw.database.host; // No error even if raw is null
}
// GOOD — unknown forces runtime checksfunction parseConfig(raw: unknown): { host: string; port: number } {
if (
typeof raw === "object" && raw !== null &&
"database"in raw &&
typeof (raw as any).database?.host === "string"
) {
const db = (raw as { database: { host: string; port: number } }).database;
return db;
}
thrownew Error("Invalid config shape");
}
// BEST — use a validation library// const config = ConfigSchema.parse(raw);
output
// any: zero safety. unknown: safe. Validated schema: safest.
Note any is infectious — any value that touches any also becomes any, spreading unsafety silently through your codebase. Use eslint rule @typescript-eslint/no-explicit-any to catch it. Legitimate uses: interfacing with very dynamic libraries, or as a temporary measure during migration.
any abuseavoid anyunknown vs anytype safetyno explicit any
Unnecessary / Incorrect Generics
syntax
// BAD: generic that adds no valuefunction bad<T extendsstring>(s: T): T { return s; }
// GOOD: just use the concrete typefunction good(s: string): string { return s; }
example
// BAD — T is unused, just makes the signature confusingfunction logValue<T>(value: T): void {
console.log(value);
}
// GOOD — no generic neededfunction logValue(value: unknown): void {
console.log(value);
}
// BAD — generic is always forced to one type anywayfunction fetchUser<T extends User>(id: string): Promise<T> { ... }
// GOOD — just return the concrete typefunction fetchUser(id: string): Promise<User> { ... }
// GOOD use of generics — T connects input to outputfunction pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
return items.map(item => item[key]);
}
output
// Use generics when T appears in at least two positions (input→output link)
Note A generic type parameter is useful when it connects two or more positions: input to output, or one parameter to another. If T appears in only one spot, a concrete type (or unknown) is simpler and clearer. Over-genericizing is a common code smell in TypeScript.
unnecessary genericwrong genericwhen to use genericsgeneric code smellsimplify generics
Enum Pitfalls
syntax
// Numeric enum accepts any numberenum Status { Active = 0, Inactive = 1 }
const s: Status = 999; // No error!
example
// PITFALL 1: Numeric enums accept any numberenum Priority { Low, Medium, High }
const p: Priority = 42; // Compiles fine! No type error.// PITFALL 2: const enums break with isolatedModulesconstenum Dir { Up, Down } // Error with isolatedModules// PITFALL 3: Enums are not tree-shakeableenum Color { Red, Green, Blue }
// Compiles to a runtime object — bundlers cannot remove unused members// ALTERNATIVE: Use string literal unionstype Priority = "low" | "medium" | "high";
const p: Priority = "low";
// p = "whatever"; // Error — properly type-safe
output
// String literal unions are simpler, safer, and more compatible
Note Numeric enums have a type safety gap — any number is assignable. String enums are safer but add runtime overhead. const enums are erased but break with Babel/esbuild/SWC. For most cases, string literal unions (optionally with an as const object for runtime values) are the pragmatic choice.
enum pitfallenum problemnumeric enum bugenum vs unionenum tree shaking
Type Assertion vs Type Guard
syntax
// DANGEROUS: assertion (no runtime check)const user = data as User;
// SAFE: type guard (runtime check)if (isUser(data)) { /* data is User */ }
example
interface User {
id: string;
name: string;
email: string;
}
// BAD: assertion — no runtime check, crashes if wrongconst user = JSON.parse(rawJson) as User;
console.log(user.email.toUpperCase()); // Runtime crash if email is missing// GOOD: type guard — verifies at runtimefunction isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"id"in value &&
"name"in value &&
"email"in value
);
}
const data = JSON.parse(rawJson);
if (isUser(data)) {
console.log(data.email.toUpperCase()); // Safe
} else {
console.error("Invalid user data");
}
output
// Assertions trust you blindly; guards verify at runtime
Note Type assertions (as Type) are a compile-time lie — they produce zero runtime code. Type guards produce actual runtime checks. Always prefer guards for external data (API responses, user input, file reads). Assertions are acceptable for values you genuinely control (e.g., DOM elements you know exist).
assertion vs guardas vs type guardruntime checksafe castvalidate type
// These all cause implicit any errors with noImplicitAny:// Function parameters without annotationsfunction greet(name) { // Error: Parameter 'name' implicitly has 'any' typereturn`Hello, ${name}`;
}
// Destructured parametersfunction render({ title, body }) { // Error on bothreturn`<h1>${title}</h1><p>${body}</p>`;
}
// Fix: always annotate function parametersfunction greet(name: string) {
return`Hello, ${name}`;
}
function render({ title, body }: { title: string; body: string }) {
return`<h1>${title}</h1><p>${body}</p>`;
}
output
// noImplicitAny (enabled by strict: true) catches unannotated parameters
Note noImplicitAny is part of strict mode and catches function parameters that would silently become any. This is one of the most important checks — without it, many functions quietly lose all type safety. Callback parameters in typed contexts (like .map(), .filter()) are inferred and do not trigger this error.
implicit anynoImplicitAnymissing annotationparameter anystrict any
Trusting readonly at Runtime
syntax
// readonly is compile-time onlyconst arr: readonly number[] = [1, 2, 3];
(arr asnumber[]).push(4); // No runtime protection
example
interface Config {
readonly apiKey: string;
readonly maxRetries: number;
}
const config: Config = { apiKey: "secret", maxRetries: 3 };
// TypeScript prevents this:// config.apiKey = "changed"; // Error// But at runtime, nothing stops this:
(config as any).apiKey = "changed"; // Works at runtime!
console.log(config.apiKey); // "changed"// For real immutability, use Object.freeze:const safeConfig = Object.freeze({ apiKey: "secret", maxRetries: 3 });
// safeConfig.apiKey = "x"; // Runtime TypeError + compile error
output
// readonly is erased at runtime — use Object.freeze for true immutability
Note readonly, Readonly<T>, and ReadonlyArray are all compile-time only. They are stripped during compilation and provide zero runtime enforcement. Code that bypasses the type system (any casts, JavaScript interop) can still mutate. For sensitive data, combine readonly with Object.freeze or structuredClone.
readonly runtimeimmutability mistakeObject.freezereadonly limitationcompile time only
Structural Typing Surprises
syntax
// TypeScript uses structural (shape) typing, not nominal (name) typing
example
interface Cat {
name: string;
purr(): void;
}
interface Robot {
name: string;
purr(): void;
}
// These are the SAME type to TypeScript!const robot: Robot = { name: "RoboCat", purr() { console.log("bzzz"); } };
const cat: Cat = robot; // No error — shapes match// Excess property checking only works on object literals:const direct: Cat = {
name: "Whiskers",
purr() {},
// batteries: true, // Error: Object literal may only specify known properties
};
// But NOT on variables:const obj = { name: "X", purr() {}, batteries: true };
const sneaky: Cat = obj; // No error — extra properties allowed from variables
output
// Two types with the same shape are interchangeable, regardless of name
Note Structural typing means TypeScript cares about shape, not name. Excess property checking only catches extra properties on direct object literals — not on variables. This often surprises developers from C#/Java backgrounds. Use branded types if you need nominal distinction.