TypeScript type Keyword
last modified October 15, 2026
The type keyword creates a type alias: a name for any
type. The basics of type aliases are covered in the
type aliases tutorial. This tutorial
looks at three topics that come up as soon as aliases are used in real code:
how type differs from interface, how aliases take
type parameters, and how an alias can refer to itself to describe nested
data.
An Alias for Any Type
Unlike an interface, which always describes an object, a type alias can name any type.
type Id = string | number;
type Point = [x: number, y: number];
type Formatter = (value: number) => string;
type Status = "active" | "inactive";
const userId: Id = "u-42";
const start: Point = [0, 0];
const euro: Formatter = value => `${value.toFixed(2)} €`;
const accountStatus: Status = "active";
console.log(userId, start, euro(9.5), accountStatus);
The aliases name a union, a tuple, a function type, and a union of
literals. None of these can be written as an interface. An alias does not
create a new type; Id is just another name for
string | number, and the two are fully interchangeable.
$ npx tsx basic_type_alias.ts u-42 [ 0, 0 ] 9.50 € active
type and interface for Object Shapes
For object types, type and interface are
largely equivalent.
type UserType = {
name: string;
age: number;
greet(): string;
};
interface UserInterface {
name: string;
age: number;
greet(): string;
}
const a: UserType = { name: "Alice", age: 30, greet: () => "Hi from Alice" };
const b: UserInterface = a;
console.log(a.greet(), b.age);
Both declarations describe the same shape. Because TypeScript compares types by structure, an object of one type can be assigned to a variable of the other. The differences appear when the types are extended, merged, or combined with other kinds of types.
$ npx tsx object_shapes.ts Hi from Alice 30
Extending Types
Interfaces are extended with extends. Type aliases are
combined with the intersection operator &. The two can be
mixed.
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
type Pet = {
name: string;
};
type Cat = Pet & {
indoor: boolean;
};
interface ShowCat extends Cat {
awards: number;
}
const dog: Dog = { name: "Rex", breed: "Beagle" };
const cat: ShowCat = { name: "Tom", indoor: true, awards: 3 };
console.log(dog, cat);
An interface can extend another interface, and also a type alias of
an object type, as ShowCat does. A type alias can intersect
interfaces as well. In most cases, the result is the same.
$ npx tsx extending.ts
{ name: 'Rex', breed: 'Beagle' } { name: 'Tom', indoor: true, awards: 3 }
Conflicts When Extending
The difference shows when the extended types disagree about a property. An interface reports the conflict immediately.
interface Base {
id: string;
}
interface Derived extends Base {
id: number;
}
With an intersection, the conflicting property is not reported. It
silently gets the type string & number, which is
never.
$ npx tsc --noEmit
extends_conflict.ts(5,11): error TS2430: Interface 'Derived' incorrectly extends interface 'Base'.
Types of property 'id' are incompatible.
Type 'number' is not assignable to type 'string'.
type Base = { id: string };
type Derived = Base & { id: number };
const item: Derived = { id: 1 };
The error appears only when a value is created, and its message is
about never, far from the cause. This is one reason why
interfaces are often recommended for object types that extend each other:
their errors are earlier and clearer, and the compiler checks
extends relationships faster than large intersections.
$ npx tsc --noEmit intersection_conflict.ts(4,25): error TS2322: Type 'number' is not assignable to type 'never'.
Declaration Merging
Two interfaces with the same name in the same scope are merged into one. Type aliases cannot be declared twice.
interface Settings {
theme: string;
}
interface Settings {
fontSize: number;
}
const settings: Settings = { theme: "dark", fontSize: 14 };
console.log(settings);
The two declarations of Settings are combined, so the
object must have both properties. This is how libraries let users add
properties to existing interfaces, for example to the global
Window interface. See the
declaration merging
tutorial.
$ npx tsx interface_merging.ts
{ theme: 'dark', fontSize: 14 }
type Settings = { theme: string };
type Settings = { fontSize: number };
A type alias is closed: once declared, it cannot be extended in place. Whether this is an advantage depends on the situation; it prevents accidental merging, but it also prevents intentional augmentation.
$ npx tsc --noEmit duplicate_type.ts(1,6): error TS2300: Duplicate identifier 'Settings'. duplicate_type.ts(2,6): error TS2300: Duplicate identifier 'Settings'.
What Only type Can Do
Several kinds of types can only be named with a type alias.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number };
type Size = "S" | "M" | "L";
type SizeLabel = `size-${Lowercase<Size>}`;
type Optional<T> = { [K in keyof T]?: T[K] };
type ElementOf<T> = T extends readonly (infer E)[] ? E : never;
const shape: Shape = { kind: "circle", radius: 2 };
const label: SizeLabel = "size-m";
const draft: Optional<{ title: string; body: string }> = { title: "Hello" };
const first: ElementOf<string[]> = "only strings";
console.log(shape.kind, label, draft, first);
Shape is a union of object types,
SizeLabel a template literal type that evaluates to
"size-s" | "size-m" | "size-l", Optional a mapped
type, and ElementOf a conditional type with
infer. An interface can describe only one object shape, so all
of these require type.
$ npx tsx type_only.ts
circle size-m { title: 'Hello' } only strings
Classes and Type Aliases
A class can implement an interface or a type alias, as long as the alias describes an object type.
type Printable = {
print(): string;
};
interface Comparable<T> {
compareTo(other: T): number;
}
class Money implements Printable, Comparable<Money> {
constructor(private cents: number) {}
print(): string {
return `${(this.cents / 100).toFixed(2)} €`;
}
compareTo(other: Money): number {
return this.cents - other.cents;
}
}
const prices = [new Money(1999), new Money(499), new Money(1200)];
prices.sort((x, y) => x.compareTo(y));
console.log(prices.map(p => p.print()).join(", "));
Money implements both the Printable alias
and the generic Comparable interface. A class cannot implement
an alias of a union type, because it would not be clear which member the
class should satisfy.
$ npx tsx implements_type.ts 4.99 €, 12.00 €, 19.99 €
Choosing Between type and interface
- Use
interfacefor object shapes that are public API, are extended by other types, or are meant to be augmented by declaration merging. - Use
typefor unions, tuples, function types, literal types, and anything built with mapped, conditional, or template literal types. - Within one code base, pick one style for plain object shapes and use it consistently; both are fine.
Generic Type Aliases
A type alias can have type parameters, written in angle brackets. They are filled in when the alias is used.
type Result<T, E = string> =
| { ok: true; value: T }
| { ok: false; error: E };
type Pair<A, B = A> = [first: A, second: B];
type Nullable<T> = T | null;
function parseNumber(text: string): Result<number> {
const n = Number(text);
return Number.isNaN(n) ? { ok: false, error: `not a number: ${text}` } : { ok: true, value: n };
}
for (const input of ["42", "abc"]) {
const result = parseNumber(input);
console.log(result.ok ? `value ${result.value}` : `error: ${result.error}`);
}
const range: Pair<number> = [1, 10];
const entry: Pair<string, number> = ["age", 30];
const middleName: Nullable<string> = null;
console.log(range, entry, middleName);
Result<T, E> describes the outcome of an
operation that can fail: a success with a value of type T or a
failure with an error of type E. The ok property
makes it a discriminated union, so checking result.ok narrows the
type and gives access to value or error.
Type parameters can have defaults. Result<number>
uses string for the error, and Pair<number>
uses number for both elements, because the default of
B is A.
$ npx tsx generic_alias.ts value 42 error: not a number: abc [ 1, 10 ] [ 'age', 30 ] null
Constraints on Type Parameters
The extends keyword restricts which types can be passed
to a type parameter.
type Entity = { id: number };
type Repository<T extends Entity> = {
items: T[];
findById(id: number): T | undefined;
};
type KeysOfType<T, V> = {
[K in keyof T]: T[K] extends V ? K : never;
}[keyof T];
type Book = { id: number; title: string; author: string; year: number };
const books: Repository<Book> = {
items: [
{ id: 1, title: "Dune", author: "Herbert", year: 1965 },
{ id: 2, title: "Neuromancer", author: "Gibson", year: 1984 },
],
findById(id) {
return this.items.find(item => item.id === id);
},
};
const textField: KeysOfType<Book, string> = "author";
console.log(books.findById(2)?.title, books.findById(3));
console.log(textField);
Repository accepts only types with a numeric
id, which its findById method relies on;
Repository<string> would be an error. The
KeysOfType alias combines a mapped type, a conditional type, and
an indexed access to compute the keys whose values have a given type. For
Book and string, it is
"title" | "author".
$ npx tsx generic_constraints.ts Neuromancer undefined author
Recursive Type Aliases
A type alias can refer to itself. This describes data of arbitrary depth, such as trees.
type TreeNode<T> = {
value: T;
children: TreeNode<T>[];
};
function sum(node: TreeNode<number>): number {
return node.value + node.children.reduce((acc, child) => acc + sum(child), 0);
}
function depth<T>(node: TreeNode<T>): number {
return 1 + Math.max(0, ...node.children.map(depth));
}
const tree: TreeNode<number> = {
value: 1,
children: [
{ value: 2, children: [{ value: 4, children: [] }] },
{ value: 3, children: [] },
],
};
console.log(sum(tree), depth(tree));
Each TreeNode holds a value and an array of child nodes
of the same type. The recursive functions follow the structure of the type:
sum adds the value of a node to the sums of its children, and
depth counts the levels. The compiler checks every nested level
of the tree literal.
$ npx tsx recursive_tree.ts 10 3
A Type for JSON Data
Recursive aliases can be unions. A classic example is a type that describes any valid JSON value.
type Json =
| string
| number
| boolean
| null
| Json[]
| { [key: string]: Json };
function countValues(value: Json): number {
if (Array.isArray(value)) {
return value.reduce((acc: number, item) => acc + countValues(item), 0);
}
if (typeof value === "object" && value !== null) {
return Object.values(value).reduce((acc: number, item) => acc + countValues(item), 0);
}
return 1;
}
const config: Json = {
name: "app",
version: 3,
features: ["search", "export"],
database: { host: "localhost", ports: [5432, 5433], ssl: false },
};
console.log(countValues(config));
A Json value is a primitive, null, an array
of Json values, or an object whose properties are
Json values. Values such as functions, Date
objects, or undefined cannot be assigned to it, which roughly
matches what JSON.stringify can represent. The function counts the primitive
values at any depth; here there are eight.
$ npx tsx recursive_json.ts 8
Recursive Generic Types
Recursion and generics combine into utility types that work on
nested structures. DeepReadonly makes every level of an object
read-only.
type DeepReadonly<T> = T extends (infer E)[]
? readonly DeepReadonly<E>[]
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
type Config = {
server: { host: string; ports: number[] };
debug: boolean;
};
const config: DeepReadonly<Config> = {
server: { host: "localhost", ports: [80, 443] },
debug: false,
};
config.debug = true;
config.server.host = "example.com";
config.server.ports.push(8080);
For arrays, the type produces a readonly array of deeply readonly
elements; for objects, it marks every property as readonly and applies itself
to the property types; primitives are returned unchanged. All three
modifications are rejected, including those on nested objects and arrays. The
built-in Readonly type would catch only the first one.
$ npx tsc --noEmit deep_readonly.ts(17,8): error TS2540: Cannot assign to 'debug' because it is a read-only property. deep_readonly.ts(18,15): error TS2540: Cannot assign to 'host' because it is a read-only property. deep_readonly.ts(19,21): error TS2339: Property 'push' does not exist on type 'readonly number[]'.
Recursive Conditional Types
A conditional type can call itself to unwrap nested types step by step.
type Flatten<T> = T extends readonly (infer E)[] ? Flatten<E> : T;
type Nested = number[][][];
type Leaf = Flatten<Nested>;
function flattenAll<T>(values: readonly T[]): Flatten<T>[] {
return values.flat(Infinity) as Flatten<T>[];
}
const nested: Nested = [[[1, 2], [3]], [[4]]];
const flat: Leaf[] = flattenAll(nested);
console.log(flat);
Flatten<number[][][]> removes one array level at
a time until it reaches number. The flattenAll
function uses the same type for its result. The assertion is needed because
the built-in type of flat(Infinity) is less precise. TypeScript
limits the depth of such recursion, so recursive types should stay
reasonably simple.
$ npx tsx flatten.ts [ 1, 2, 3, 4 ]
Best Practices
- Use interface for Extendable Objects: Prefer interfaces for object shapes that other types extend; errors are clearer than with intersections.
- Use type for Everything Else: Unions, tuples, functions, and computed types can only be written as aliases.
- Name Type Parameters Clearly: Single letters are fine for simple aliases; use names such as
TValuewhen there are several parameters. - Add Constraints and Defaults: Constraints document what an alias needs; defaults make common cases shorter.
- Keep Recursive Types Simple: Deeply recursive conditional types slow down the compiler and produce long error messages.
Source
TypeScript Handbook: Differences Between Type Aliases and Interfaces
This tutorial covered the type keyword in depth: how
type aliases compare with interfaces, and how generic and recursive aliases
describe reusable and nested structures.
Author
List all TypeScript tutorials.