A Type Alias Names Anything
A type alias gives a name to a type — any type. That is the difference from an interface in one sentence. An interface can only describe an object shape; an alias can name a union, a primitive, a tuple, a function signature, or an object shape, and it can be built out of other types with operators.
The keyword is type, followed by a name, an equals sign and the type itself. It creates no new type at runtime and no new type in the type system either — type Username = string really is just another word for string, and the two are freely interchangeable. That matters: naming a type does not make it distinct. If you want UserId and OrderId to be incompatible, you need the branded-type trick from the best-practices lesson.
Even so, naming is worth it for readability. A function signed (id: UserId, status: OrderStatus) tells a reader far more than (id: number, status: string), and when the underlying type changes there is one line to edit.
// A name for a primitive
type Pincode = string;
// A name for a union — an interface cannot do this
type ID = string | number;
type OrderStatus = "placed" | "packed" | "shipped" | "delivered";
// A name for an object shape
type Point = {
x: number;
y: number;
};
// A name for a function signature
type Formatter = (input: string) => string;
const toUpper: Formatter = s => s.toUpperCase();
// A name for a tuple
type Coordinate = [latitude: number, longitude: number];
// Aliases are interchangeable with what they alias
type Username = string;
const a: Username = "ananya";
const b: string = a; // OK — they are the same type - Type aliases are erased along with everything else. There is no
OrderStatusobject at runtime, so you cannot list its values — that is the trade-off against an enum discussed in the enums lesson.
interface or type? An Honest Comparison
For describing a plain object, interface User { name: string } and type User = { name: string } do the same job, and either will serve you well. Most of the internet argument about this is not worth your time. There are, however, a few real differences, and they lead to a simple rule.
Interfaces can be reopened. Declare the same interface twice and the declarations merge, which is what lets you augment a type from a library you do not control. Type aliases cannot — a duplicate type is an error — and for internal code that is usually a feature, because nobody can silently extend your type from another file.
Type aliases can express things interfaces cannot: unions, tuples, primitives, template literal types, conditional types, and anything built with type-level operators. Every utility type you will meet later is a type alias. So the rule most teams settle on is: use an interface for an object shape, especially a public one; use a type for everything else, and switch to type whenever the shape becomes a union.
- Both describe object shapes, both work with classes via
implements, both support optional andreadonlymembers - Only
typecan name a union, an intersection, a tuple, a primitive or a template literal type - Only
interfacemerges across declarations — essential for augmenting library types, hazardous by accident interfaceusesextends;typecombines with&, and the two behave differently on conflicts- An
interfacecan extend atypealias of an object shape, and atypecan intersect aninterface— mixing them is fine - Error messages tend to be shorter with a named
interface, because the compiler prints the name instead of expanding the whole shape
- The one rule that actually matters is consistency. Pick a convention, write it in the project's README, and stop relitigating it in code review.
Intersections, and How They Differ From extends
The & operator builds an intersection: a type that must satisfy everything on both sides at once. Reading it aloud helps, because the symbol looks like it should mean "or" to people used to sets — A & B means an object that has all of A's members and all of B's.
This is the alias equivalent of extends, and for the ordinary case where the two sides do not overlap, the result is identical. The difference appears on a conflict. If two interfaces declare the same property with different types, extends reports an error immediately and names the offending member. An intersection does not: it quietly computes the intersection of those two property types, and for two unrelated primitives that is never — a type no value can have.
The effect is that a bad intersection compiles, and the error surfaces later at whatever line tries to construct the object, with a message about never that gives no hint where the clash came from. It is not a common bug, but it is a memorably confusing one, and it is the strongest practical argument for using extends when both sides are object shapes you control.
Intersections are at their best combining independent pieces: a base record with timestamp fields, a set of shared component props with the specific ones.
type Timestamps = { createdAt: Date; updatedAt: Date };
type User = { id: number; name: string };
// Everything from both sides
type StoredUser = User & Timestamps;
const u: StoredUser = {
id: 1,
name: "Rahul",
createdAt: new Date(),
updatedAt: new Date()
};
// The conflict case: this line compiles without complaint
type Clash = { value: string } & { value: number };
// 'value' is now string & number, which is never
const c: Clash = { value: "hello" };
// ERROR: Type 'string' is not assignable to type 'never'.
// ...and nothing points at the declaration that caused it.
// With interfaces, the error lands on the declaration itself
interface Base { value: string }
interface Derived extends Base { value: number }
// ERROR: Interface 'Derived' incorrectly extends interface 'Base'. - Intersecting two unrelated primitives is always
never:string & numberdescribes a value that is both, and no such value exists. Seeingneverin an error message is usually a sign that an intersection somewhere has collapsed.
Generic and Recursive Aliases
A type alias can take parameters, written in angle brackets, exactly like a function takes arguments. type ApiResult<T> is a shape with a hole in it that you fill at the point of use. This is the everyday way to describe an API envelope, a paginated list, or a result that is either data or an error — one definition, reused for every payload type in your application.
Aliases may also refer to themselves, which lets you describe data of arbitrary depth. The classic example is JSON: a JSON value is a string, a number, a boolean, null, an array of JSON values, or an object whose values are JSON values. That definition is circular in English and circular in TypeScript, and it works.
A recursive alias like Json is a genuinely useful thing to keep around. It is the correct type for the result of JSON.parse when you have not yet validated the shape — far better than any, because it forces you to narrow before use while still describing everything JSON can contain.
// A generic envelope, reused for every payload
type ApiResult<T> =
| { ok: true; data: T }
| { ok: false; error: string };
type UserResult = ApiResult<{ id: number; name: string }>;
type CourseListResult = ApiResult<string[]>;
function render(result: UserResult): string {
return result.ok ? result.data.name : `Failed: ${result.error}`;
}
// Generic aliases can have defaults, like function parameters
type Paged<T, Meta = { page: number; total: number }> = {
items: T[];
meta: Meta;
};
// Recursive: a type that refers to itself
type Json =
| string
| number
| boolean
| null
| Json[]
| { [key: string]: Json };
const config: Json = {
name: "priodemy",
limits: { daily: 5, tags: ["free", "trial"] },
active: true
}; Template Literal Types
A template literal type builds string types from other string types, using the same backtick syntax as template strings in JavaScript. Put a union inside the placeholder and TypeScript expands every combination, so two unions of three and two members produce a union of six exact strings.
This is more useful than it first appears, because a lot of real APIs are string patterns: CSS class names like text-lg, event names like onClick, route paths like /courses/:id, translation keys. Modelling them as template literal types means a typo in a class name is a compile error rather than a style that silently does nothing.
Four helpers come built in for changing case — Uppercase, Lowercase, Capitalize and Uncapitalize — and they combine with template literals to derive one naming convention from another. Deriving handler prop names from event names is the standard demonstration, and it is exactly what typed component libraries do.
Keep them in proportion. Template literal types are excellent for a fixed vocabulary you control. Trying to validate a free-form string such as an email address at the type level is not what they are for; that is a runtime job.
type Shade = "light" | "dark";
type Color = "red" | "green" | "blue";
type Variant = `${Shade}-${Color}`;
// "light-red" | "light-green" | "light-blue"
// | "dark-red" | "dark-green" | "dark-blue"
let v: Variant = "dark-blue"; // OK
let w: Variant = "bright-red"; // ERROR: not assignable to type 'Variant'
// Case helpers, used to derive one convention from another
type EventNames = "click" | "focus" | "input";
type HandlerName = `on${Capitalize<EventNames>}`;
// "onClick" | "onFocus" | "onInput"
const handlers: Record<HandlerName, () => void> = {
onClick: () => {},
onFocus: () => {},
onInput: () => {}
};
// A pattern that still allows any string in the placeholder
type LessonPath = `/courses/${string}/${string}`;
const path: LessonPath = "/courses/typescript/generics"; // OK - A union inside a template literal multiplies out. Three placeholders with ten options each produce a thousand-member union, and the compiler will slow down noticeably. Keep the vocabularies small.
Deriving Types Instead of Repeating Them
The habit that separates comfortable TypeScript from tedious TypeScript is deriving types from things that already exist, rather than writing them out a second time. Three small operators do most of this work, and they appear constantly in real codebases.
typeof in a type position asks for the type of an existing value. This is not JavaScript's runtime typeof — same word, different context — and it is how you turn a configuration object, a default value or an imported constant into a type without duplicating its shape.
keyof gives you the union of an object type's keys, so keyof User is "id" | "name" | "email". Indexed access, written User["email"], gives you the type of one property. Put them together and you can write a function that accepts only real property names of a real object — checked, autocompleted, and automatically correct after a rename.
The pay-off is that these derived types cannot drift. Rename a field on User and every derived type updates with it, and every call site that used the old name becomes an error immediately.
const defaultSettings = {
theme: "dark",
fontSize: 16,
notifications: true
};
// Derive the type instead of writing it again
type Settings = typeof defaultSettings;
// { theme: string; fontSize: number; notifications: boolean }
type SettingName = keyof Settings;
// "theme" | "fontSize" | "notifications"
type FontSize = Settings["fontSize"]; // number
function describe(key: SettingName): string {
return `${key} = ${String(defaultSettings[key])}`;
}
describe("theme"); // OK, and autocompleted
describe("themes"); // ERROR: not assignable to 'SettingName'
// Same idea on a union of literals
const STATUSES = ["placed", "packed", "shipped"] as const;
type Status = typeof STATUSES[number]; - Write the value first and derive the type when the value is the source of truth — a defaults object, a route table, a colour palette. Write the type first when the type is the source of truth, as with an API contract you must match.
