JavaScript to TypeScript Types
How TypeScript's type system formalizes patterns that JavaScript developers already use informally through duck typing and typeof checks.
Duck Typing vs Structural Typing
JavaScript uses duck typing at runtime: if an object has a .quack() method, you can call it regardless of how the object was constructed. TypeScript formalizes this with structural typing: if two types have the same shape (same properties and types), they are compatible even if they have different names. This is different from nominal typing (used by Java and C#), where two classes are only compatible if they share an inheritance relationship. Structural typing means TypeScript matches how JavaScript developers already think about objects. You pass objects to functions based on what properties they have, not what class they belong to. The migration benefit is significant: you do not need to refactor class hierarchies or implement interfaces to make existing code typecheck. If your JavaScript code passes an object with { name: string, age: number } to a function, TypeScript accepts any object with those properties. Extra properties are allowed when passing object references (but not when creating object literals inline, where excess property checks apply). This "shapes match shapes" approach makes TypeScript feel like JavaScript with guardrails rather than a different language.
Structural compatibility
function greet(user) {
return "Hello " + user.name;
}
// works with any object that has .name
greet({ name: "Ada", age: 30 });
greet({ name: "Grace" });function greet(user: { name: string }) {
return `Hello ${user.name}`;
}
// works with any object that has .name
greet({ name: "Ada", age: 30 }); // excess OK via variable
const u = { name: "Ada", age: 30 };
greet(u); // Type Aliases
JavaScript has no type alias syntax -- you document object shapes in JSDoc comments or not at all. TypeScript introduces the type keyword: type User = { name: string; age: number }. A type alias is a name for any type expression, from primitives (type ID = string) to complex intersections and conditional types. Aliases do not create new runtime entities; they are purely a compile-time concept. The migration step is straightforward: identify the shapes your JavaScript code passes around and give them names. This alone improves readability because parameter types like (user: User) replace (user: any) or (user: { name: string, age: number, email: string, ... }). Type aliases also compose. You build larger types from smaller ones using intersection (&): type Admin = User & { permissions: string[] }. This additive composition mirrors how JavaScript developers extend objects with spread: { ...user, permissions: [...] }, but at the type level. Unlike interfaces (covered in the next topic), type aliases can represent unions, primitives, tuples, and mapped types -- making them the more general-purpose tool.
Type alias
// JSDoc (optional, unenforceable)
/** @typedef {{ name: string, age: number }} User */
const user = { name: "Ada", age: 30 };type User = {
name: string;
age: number;
};
const user: User = { name: "Ada", age: 30 };Composing types with intersection
const admin = {
...user,
permissions: ["read", "write"],
};type Admin = User & {
permissions: string[];
};
const admin: Admin = {
...user,
permissions: ["read", "write"],
};Union Types and Literal Types
JavaScript developers handle multiple possible types with runtime checks: if (typeof input === "string"). TypeScript captures this pattern with union types: string | number means the value is one or the other. The compiler tracks which type is active based on control flow: after if (typeof input === "string"), the variable narrows to string inside the block. This is called control flow narrowing, and it makes union types practical rather than theoretical. Literal types are a refinement: instead of string, you specify exactly which strings are valid: type Direction = "north" | "south" | "east" | "west". Passing "northeast" triggers a compile error. JavaScript developers already use string constants this way (if (dir === "north")), but TypeScript turns those constants into a type that the compiler enforces. The combination of unions and literals replaces many uses of enums. Where JavaScript might define const DIRECTIONS = { NORTH: "north", SOUTH: "south" }, TypeScript can define type Direction = "north" | "south" and get compile-time checking without a runtime object. Both approaches work in TypeScript, but string literal unions are lighter weight and produce cleaner .d.ts files.
Union type with narrowing
function format(input) {
if (typeof input === "string") {
return input.toUpperCase();
}
return input.toFixed(2);
}function format(input: string | number) {
if (typeof input === "string") {
return input.toUpperCase(); // narrowed to string
}
return input.toFixed(2); // narrowed to number
}Literal types
function move(direction) {
// no compile-time check on direction
if (direction === "north") { /* ... */ }
}
move("nroth"); // typo, no errortype Direction = "north" | "south" | "east" | "west";
function move(direction: Direction) {
if (direction === "north") { /* ... */ }
}
// move("nroth"); // compile error: typo caughtCan you write this from memory?
Declare `id` that can be string or number
typeof, keyof, and Type Narrowing
JavaScript's typeof operator returns a runtime string ("string", "number", "object", etc.). TypeScript reuses typeof at the type level: type T = typeof myVariable extracts the type that the compiler inferred for myVariable. This is useful when you want to derive a type from an existing value without manually rewriting the shape. keyof extracts the keys of an object type as a union of string literals: keyof User produces "name" | "age". Combined with indexed access types (User["name"] resolves to string), keyof enables generic property access functions: function getProperty<T, K extends keyof T>(obj: T, key: K): T[K]. JavaScript developers write the same function without generics, but the TypeScript version catches typos in key names at compile time. Type narrowing goes beyond typeof. TypeScript narrows on instanceof, in checks ("name" in obj), equality checks, and custom type guard functions (function isUser(x: unknown): x is User). Each narrowing mechanism corresponds to a runtime check that JavaScript developers already write -- TypeScript just tracks the implications through the rest of the code block.
typeof at the type level
const config = {
host: "localhost",
port: 5432,
};
// no way to extract the type staticallyconst config = {
host: "localhost",
port: 5432,
} as const;
type Config = typeof config;
// { readonly host: "localhost"; readonly port: 5432 }keyof and generic property access
function getProperty(obj, key) {
return obj[key]; // no safety
}function getProperty<T, K extends keyof T>(
obj: T, key: K
): T[K] {
return obj[key]; // type-safe
}