# 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)