ZetCode

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.

basic_type_alias.ts
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.

object_shapes.ts
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.

extending.ts
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.

extends_conflict.ts
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'.
intersection_conflict.ts
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_merging.ts
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 }
duplicate_type.ts
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_only.ts
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.

implements_type.ts
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

Generic Type Aliases

A type alias can have type parameters, written in angle brackets. They are filled in when the alias is used.

generic_alias.ts
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.

generic_constraints.ts
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.

recursive_tree.ts
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.

recursive_json.ts
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.

deep_readonly.ts
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.

flatten.ts
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

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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all TypeScript tutorials.