| technology | TypeScript | ||||||
|---|---|---|---|---|---|---|---|
| domain | frontend | ||||||
| level | Senior/Architect | ||||||
| version | 5.5+ | ||||||
| tags |
|
||||||
| ai_role | Senior TypeScript Expert | ||||||
| last_updated | 2026-03-22 |
- Primary Goal: Proper typing for objects, arrays, and functions in TypeScript applications.
- Target Tooling: Cursor, Windsurf, Antigravity.
- Tech Stack Version: TypeScript 5.5+
Note
Context: Defining maps/dictionaries.
const prices: { [key: string]: number } = { apple: 1 };The index signature syntax is more verbose, harder to read, and less semantically clear when representing dictionaries or lookups compared to utility types.
const prices: Record<string, number> = { apple: 1 };Use the Record<K, V> utility type for key-value maps. It provides a deterministic, clean, and declarative syntax that AI agents and engineers can parse instantly.
Note
Context: Passing objects to functions.
const inputData = { id: 1, name: 'A', maliciousField: true };
saveUser({ id: 1, name: 'A', maliciousField: true }); // Error: excess property
saveUser(inputData); // No error! 'maliciousField' is leaked into dbExcess property checks only apply to inline object literals. Passing pre-defined variables bypasses this compiler check, leading to data pollution or security vulnerabilities (like Mass Assignment).
// Assume User type has only `id` and `name`
const { maliciousField, ...validUser } = inputData;
saveUser(validUser);Be strictly explicit about data payload boundaries. Use destructuring to strip unknown or unsafe properties before passing objects to persistence layers or external APIs, guaranteeing deterministic data shapes.
Note
Context: Preventing accidental state mutation.
function process(config: Config) {
config.port = 80; // Side effect!
}Mutable inputs lead to unpredictable state changes, side effects, and bugs that are notoriously difficult to trace across large application boundaries.
function process(config: Readonly<Config>) {
// config.port = 80; // TS Error: Cannot assign to 'port' because it is a read-only property.
}Use Readonly<T> for function parameters and as const for configuration objects. This enforces strict immutability at compile time, guaranteeing predictable and pure function execution.
Note
Context: Getting the resolved type of a Promise.
type Result = typeof apiCall extends Promise<infer U> ? U : never;Manually unwrapping promises via custom conditional types is unnecessarily complex, less readable, and fails to properly recursively unwrap nested promises natively.
type Result = Awaited<ReturnType<typeof apiCall>>;Always use the built-in Awaited<T> utility type (TS 4.5+) for deterministic and clean promise resolution.
Note
Context: Ensuring correct context in callback-heavy code.
function handleClick(event: Event) {
this.classList.add('active'); // 'this' is implicitly 'any'
}this defaults to any in unbound functions, making it trivial to access properties that don't exist on the execution context and bypassing compiler safety nets.
function handleClick(this: HTMLElement, event: Event) {
this.classList.add('active'); // Safe, 'this' is HTMLElement
}Always type the first pseudo-parameter this explicitly in functions that rely on dynamic execution contexts. This provides a deterministic context for the compiler and Vibe Coding agents.
Note
Context: Defining class properties.
class User {
public name: string;
constructor(name: string) {
this.name = name;
}
}Redundant repetition of property names in the declaration, constructor parameter, and assignment block creates unnecessary boilerplate and increases cognitive load.
class User {
constructor(public readonly name: string) {}
}Leverage TypeScript parameter properties in constructors to declare and initialize class members in a single deterministic step, minimizing noise.
Note
Context: Defining blueprints for classes.
class BaseService {
getData() { throw new Error("Not implemented"); }
}Using concrete classes as blueprints by throwing runtime errors fails to enforce implementation contracts at compile time. Interfaces alone cannot provide shared implementation logic.
abstract class BaseService {
abstract getData(): Promise<string>;
log(msg: string) { console.log(msg); }
}| Feature | Abstract Classes | Interfaces |
|---|---|---|
| Implementation Logic | Can provide default/shared implementation | Cannot provide implementation |
| Multiple Inheritance | No (Single inheritance only) | Yes (Can implement multiple) |
| Runtime Presence | Yes (Compiles to JS class) | No (Erased at compile time) |
| Access Modifiers | Supports public, protected, private | All members are public |
Utilize abstract classes when requiring shared implementation logic combined with strict contractual enforcement of specific methods by subclasses.
Note
Context: Encapsulating data in classes.
class User {
private secret = 123;
}
// Malicious user circumvents compiler:
console.log((user as any)['secret']); // Works at runtime!TypeScript's private keyword is a compile-time illusion. At runtime, the property remains fully exposed and accessible via bracket notation or generic type stripping.
class User {
#secret = 123;
getSecretSafely() {
return this.#secret;
}
}Implement ES2020 #private fields for guaranteed runtime encapsulation, specifically when architecting SDKs, libraries, or processing highly secure data.
Note
Context: Meta-programming in TypeScript.
// Requires: "experimentalDecorators": true
function Logged(constructor: Function) { /* ... */ }
@Logged
class MyClass {}Legacy decorators rely on the deprecated experimentalDecorators flag, misaligning with the standardized ECMAScript proposal and risking future deprecation breakage.
// TC39 standard (TS 5.0+)
function Logged<Class extends new (...args: any[]) => unknown>(
value: Class,
context: ClassDecoratorContext
) { /* ... */ }
@Logged
class MyClass {}Migrate to TC39 Standard Decorators supported in TypeScript 5.0+. Unless dictated by an explicit framework architecture (like NestJS or Angular), strictly use standard implementations.
Note
Context: Transforming existing types.
interface User { name: string; age: number; role: string; }
interface UserUpdate {
name?: string;
age?: number;
}Manual re-declaration of properties leads to critical synchronization issues when the base User interface architecture evolves, creating fractured type contracts.
interface User { name: string; age: number; role: string; }
type UserUpdate = Partial<Pick<User, 'name' | 'age'>>;