JavaScript to TypeScript Interfaces
How TypeScript interfaces formalize the implicit object contracts that JavaScript developers maintain through convention and documentation.
Implicit Shapes vs Explicit Interfaces
In JavaScript, object shapes are conventions. A function that expects a "user" object with name and email relies on the caller to pass the right properties. Documentation, tests, and code review enforce the contract. TypeScript interfaces make these contracts explicit: interface User { name: string; email: string }. Declaring an interface does not generate any runtime code -- it is erased during compilation. The payoff is at development time: the compiler checks every object assigned to a User variable, every function receiving a User parameter, and every property access on a User reference. Misspelling email as emial becomes a compile error instead of a silent undefined. For JavaScript migration, interfaces are the second thing most teams add after function parameter types. You start with the most-used shapes (API responses, state objects, configuration), define interfaces for them, and gradually let the type checker catch mismatches. The refactoring burden is low because TypeScript interfaces are structural -- any existing object with the right properties satisfies the interface automatically.
Object shape convention vs interface
/** @param {{ name: string, email: string }} user */
function sendWelcome(user) {
console.log("Welcome " + user.name);
}interface User {
name: string;
email: string;
}
function sendWelcome(user: User) {
console.log(`Welcome ${user.name}`);
}Can you write this from memory?
Create Admin interface extending User with role: string
Extending Interfaces
JavaScript creates object hierarchies through prototype chains, class inheritance, or spread composition. TypeScript interfaces extend through the extends keyword: interface Admin extends User { permissions: string[] }. This adds properties from User to Admin without runtime overhead -- no class, no prototype chain, just a type-level inheritance. You can extend multiple interfaces: interface AdminAuthor extends Admin, Author { }. JavaScript has no compile-time equivalent; you would merge objects at runtime and rely on convention to track which properties belong to which "parent." The extends pattern is especially useful for API types. A base response interface (interface ApiResponse { status: number }) can be extended by specific endpoints (interface UserResponse extends ApiResponse { data: User }). This mirrors how JavaScript developers already structure response types, but with compiler enforcement that prevents forgetting required fields. Interface extension and type intersection (Type = A & B) achieve similar results. The difference: interfaces produce clearer error messages (the compiler says "missing property X from interface Y") and support declaration merging, while intersections are more flexible for ad-hoc combinations.
Extending an interface
const admin = {
...user,
permissions: ["read", "write"],
};
// shape is implicit, no validationinterface Admin extends User {
permissions: string[];
}
const admin: Admin = {
name: "Ada",
email: "ada@example.com",
permissions: ["read", "write"],
};Multiple extends
const adminAuthor = {
...admin,
...author,
};interface Author {
posts: string[];
}
interface AdminAuthor extends Admin, Author {}
const aa: AdminAuthor = {
name: "Ada",
email: "ada@example.com",
permissions: ["write"],
posts: ["First post"],
};Optional Properties and Index Signatures
JavaScript objects can have any combination of properties, and accessing a missing property returns undefined. TypeScript interfaces mark optional properties with ?: interface Config { host: string; port?: number }. The port property becomes number | undefined in the type system, forcing callers to handle the missing case. This is one of the highest-value migrations from JavaScript: marking which fields are guaranteed versus which might be absent prevents an entire class of "Cannot read property of undefined" runtime errors. Index signatures describe objects used as dictionaries: interface StringMap { [key: string]: number }. This tells TypeScript that any string key maps to a number value. JavaScript objects used as maps (const counts = {}; counts["apple"] = 3) are technically record-like, and index signatures formalize that pattern. An index signature combined with named properties creates a hybrid: interface Scores { default: number; [student: string]: number }. The named property must be compatible with the index signature type. JavaScript developers who use objects as lookup tables will find index signatures a natural fit. For more structured key-value needs, TypeScript also supports Map types and the Record<K, V> utility type.
Optional property
function connect(config) {
const host = config.host;
const port = config.port || 5432;
}interface DbConfig {
host: string;
port?: number;
}
function connect(config: DbConfig) {
const host = config.host;
const port = config.port ?? 5432;
}Index signature
const scores = {};
scores["Alice"] = 95;
scores["Bob"] = 87;interface Scores {
[student: string]: number;
}
const scores: Scores = {};
scores["Alice"] = 95;
scores["Bob"] = 87;Interface vs Type Alias
TypeScript offers two ways to name object shapes: interface and type. For basic object types, they are interchangeable: interface User { name: string } and type User = { name: string } produce identical behavior. The differences emerge at the edges. Interfaces support declaration merging -- writing interface User twice adds the properties together. This is how TypeScript extends global types (Window, NodeJS.ProcessEnv) and how library authors augment their own types across files. Type aliases do not merge; redeclaring a type alias is an error. On the other hand, type aliases can represent anything: unions (type Result = Success | Failure), tuples (type Pair = [string, number]), primitives (type ID = string), mapped types, and conditional types. Interfaces are limited to object shapes. The community convention has shifted over time. Early TypeScript style guides preferred interfaces for everything, but modern practice leans toward type for most definitions and interface only when you need declaration merging or when defining a public API contract that consumers might extend. Neither choice affects runtime behavior. The decision is about readability, API design, and whether you need merging. For migration, pick one and be consistent within a codebase -- mixing them for equivalent purposes adds cognitive load without benefit.
Equivalent declarations
// JavaScript: no distinction
const user = { name: "Ada", age: 30 };// Interface
interface User {
name: string;
age: number;
}
// Type alias (equivalent for objects)
type User2 = {
name: string;
age: number;
};Declaration merging (interface only)
// not possible in plain JavaScriptinterface Window {
myCustomProp: string;
}
// merges with global Window interface
// window.myCustomProp is now typedCan you write this from memory?
Create Admin interface extending User with role: string