// BAD: any everywherefunctionprocess(data: any): any {...}// GOOD: use proper typesfunctionprocess(data: unknown):Result{...}
Example
// BAD - any silently disables all type safetyfunctionparseConfig(raw: any){return raw.database.host;// No error even if raw is null}// GOOD - unknown forces runtime checksfunctionparseConfig(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;}thrownewError("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<Textends string>(s:T):T{return s;}// GOOD: just use the concrete typefunctiongood(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 neededfunctionlogValue(value: unknown):void{console.log(value);}// BAD - generic is always forced to one type anywayfunction fetchUser<TextendsUser>(id: string):Promise<T>{...}// GOOD - just return the concrete typefunctionfetchUser(id: string):Promise<User>{...}// GOOD use of generics - T connects input to outputfunction pluck<T,KextendskeyofT>(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 numberenumStatus{Active=0,Inactive=1}const s:Status=999;// No error!
Example
// PITFALL 1: Numeric enums accept any numberenumPriority{Low,Medium,High}const p:Priority=42;// Compiles fine! No type error.// PITFALL 2: const enums break with isolatedModulesconstenumDir{Up,Down}// Error with isolatedModules// PITFALL 3: Enums are not tree-shakeableenumColor{Red,Green,Blue}// Compiles to a runtime object - bundlers cannot remove unused members// ALTERNATIVE: Use string literal unionstypePriority="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 asUser;// SAFE: type guard (runtime check)if(isUser(data)){/* data is User */}
Example
interfaceUser{
id: string;
name: string;
email: string;}// BAD: assertion - no runtime check, crashes if wrongconst user =JSON.parse(rawJson)asUser;console.log(user.email.toUpperCase());// Runtime crash if email is missing// GOOD: type guard - verifies at runtimefunctionisUser(value: unknown): value isUser{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
Implicit any
Syntax
// BAD: no annotation, no inference → implicit anyfunctionprocess(data){...}// GOOD: annotate parametersfunctionprocess(data: string){...}
Example
// These all cause implicit any errors with noImplicitAny:// Function parameters without annotationsfunctiongreet(name){// Error: Parameter 'name' implicitly has 'any' typereturn`Hello, ${name}`;}// Destructured parametersfunctionrender({ title, body }){// Error on bothreturn`<h1>${title}</h1><p>${body}</p>`;}// Fix: always annotate function parametersfunctiongreet(name: string){return`Hello, ${name}`;}functionrender({ 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 as number[]).push(4);// No runtime protection
Example
interfaceConfig{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
interfaceCat{
name: string;purr():void;}interfaceRobot{
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.