# Types and contracts

# Infer locally, describe boundaries

TypeScript can infer simple values. Explicit parameter and return types make function contracts visible:

const course = 'frontend';
const scores: number[] = [80, 95];

function greeting(name: string, title?: string): string {
  return `Hello, ${title ? `${title} ` : ''}${name}`;
}

The optional title may be undefined. Annotating every obvious local variable adds noise; annotating inputs, reusable data shapes and public functions helps readers.

# Unions, literals and narrowing

A union accepts alternatives; a literal type restricts the exact values. Narrow the alternatives before using operations specific to one type:

type StudentId = number | string;
type LoadState = 'idle' | 'loading' | 'ready';

function formatId(id: StudentId): string {
  return typeof id === 'number' ? id.toFixed(0) : id.trim();
}
Type/operation Meaning
any Disables useful checking for this value; mistakes can propagate
unknown A value exists, but must be checked before use
value as SomeType Tells the checker to trust an assertion; does not convert or validate
typeof, instanceof, property checks Runtime checks that can also narrow the static type

For example, 42 as unknown as string does not create a string. Use conversion such as String(42) when conversion is intended.

# Structural contracts

An interface describes an object's shape. Matching that shape is sufficient; the object need not inherit from a particular class:

interface Greeter {
  greeting(): string;
}

const person: Greeter = {
  greeting: () => 'Hello, Ada'
};

A type alias can also describe an object, and can name unions and other type expressions. Interfaces are convenient for extensible object contracts; neither exists as a runtime validator. Prefer a clear contract over a rule that one spelling must always be used.

# Classes and generics

The following src/Student.ts exports a reusable class. A generic preserves the caller's chosen ID type rather than replacing it with any:

export interface Greeter {
  greeting(): string;
}

class Person {
  constructor(protected name: string) {}

  greeting(): string {
    return `Hello, ${this.name}`;
  }
}

export class Student<Id> extends Person implements Greeter {
  constructor(name: string, public readonly id: Id) {
    super(name);
  }
}
const student = new Student('Ada', 1);
const id: number = student.id;
const greeter: Greeter = student;

Use the last snippet where Student and Greeter are in scope; the next page shows explicit imports. extends inherits behavior; implements only checks a contract and supplies no methods. Constructor parameter properties declare and initialize fields without repetitive assignments.

public, protected, private and readonly constrain TypeScript access or assignment. TypeScript private is not a runtime security boundary; JavaScript #private fields enforce runtime privacy. Likewise, readonly does not deeply freeze an object.

# Recognize decorators, do not mix their models

An annotation such as @trace can decorate a class or member. Legacy TypeScript decorators enabled by experimentalDecorators and the newer decorator support introduced in TypeScript 5 have different signatures and capabilities. They are optional advanced material, not required for the examples here; use documentation matching the chosen model rather than combining snippets from both.

Try it: create Student<string> and Student<number> values. Which ID operations are allowed on each? Why can an object literal implement the Greeter contract without using implements?

TypeScript narrowing (opens new window) · Classes (opens new window) · Decorator version note (opens new window)