TypeScript Under the Hood
TypeScript extends JavaScript by adding optional static types and modern ECMAScript features. The compiler emits clean JavaScript by erasing type annotations, so type checking only happens during development.
Installation and First Steps
Install globally with npm:
npm install -g typescript
Check the version:
tsc --version
Create a file app.ts:
function greetUser(name: string): string {
return `Welcome, ${name}`;
}
console.log(greetUser("Alex"));
Compile and run:
tsc app.ts
node app.js
Using the online TypeScript Playground lets you experiment with out local setup.
Fundamental Types
Primitives
Boolean, number, string, and symbol work as expected:
let isActive: boolean = true;
let score: number = 95;
let userName: string = "carol";
const idSymbol: symbol = Symbol("id");
Arrays and Tuples
Array syntax supports both shorthand and generic forms:
let points: number[] = [10, 20, 30];
let names: Array<string> = ["Alice", "Bob"];
Tuples define fixed‑length arrays with typed positions:
let entry: [string, number, boolean] = ["task", 7, true];
Enums
Numeric and string enums provide named constants:
enum Status {
Pending,
Active,
Completed
}
enum HttpMethod {
Get = "GET",
Post = "POST",
Put = "PUT"
}
const enum eliminates the generated object and inlines values:
const enum Direction {
Up,
Down,
Left,
Right
}
let move = Direction.Up; // compiles to `let move = 0`
Any and Unknown
any disables type checking entirely:
let flexible: any = 100;
flexible = "now a string";
flexible.someMethod(); // allowed
unknown is the type‑safe counterpart – all operations require narrowing:
let input: unknown;
input = "hello";
if (typeof input === "string") {
console.log(input.toUpperCase()); // OK
}
Void, Null, and Undefined
void is for functions with no return value:
function logError(message: string): void {
console.error(message);
}
null and undefined are standalone types:
let u: undefined = undefined;
let n: null = null;
Never
never represents impossible code paths – functions that never return or always throw:
function raiseError(msg: string): never {
throw new Error(msg);
}
function infiniteLoop(): never {
while (true) {}
}
Use never for exhaustive checks in discriminated unions:
type Shape = "circle" | "square";
function getArea(shape: Shape): number {
switch (shape) {
case "circle":
return Math.PI * 2 ** 2;
case "square":
return 4 ** 2;
default:
const _exhaustive: never = shape;
return _exhaustive;
}
}
Object Types
object refers to any non‑primitive:
let obj: object = { a: 1 };
{} describes an empty object but still allows access to Object.prototype members.
Type Assertions and Guards
Assertions
Tell the compiler to treat a value as a specific type:
let raw: any = "hello";
let length1: number = (raw as string).length;
let length2: number = (<string>raw).length;
Non‑null assertion ! removes null and undefined from a type:
function process(input?: string) {
const safe: string = input!; // assumes input is not null/undefined
}
Definite assignment assertion ! assures the compiler that a variable will be initialised:
let initialised!: number;
setup();
console.log(initialised * 2); // OK
function setup() {
initialised = 21;
}
Type Guards
Use runtime checks to narrow types.
typeof guard:
function format(value: string | number) {
if (typeof value === "string") {
return value.trim();
}
return value.toFixed(2);
}
instanceof guard:
class Dog {
bark() {}
}
class Cat {
meow() {}
}
function handlePet(pet: Dog | Cat) {
if (pet instanceof Dog) {
pet.bark();
} else {
pet.meow();
}
}
in guard:
interface Admin {
role: string;
permissions: string[];
}
interface User {
name: string;
age: number;
}
function displayInfo(person: Admin | User) {
if ("permissions" in person) {
console.log(person.role);
}
}
Custom type predicate:
function isString(value: unknown): value is string {
return typeof value === "string";
}
Union, Intersection, and Alias
Union Types
Allow a value to be one of several types:
type ID = string | number;
function fetchRecord(id: ID) {
// ...
}
Mixed with literal types:
type EventName = "click" | "scroll" | "resize";
Discriminated Unions
Each variant shares a common literal property to enable exhaustive narrowing:
interface Bike {
kind: "bike";
gears: number;
}
interface Car {
kind: "car";
doors: number;
}
type Vehicle = Bike | Car;
function describe(v: Vehicle) {
switch (v.kind) {
case "bike":
return `Bike with ${v.gears} gears`;
case "car":
return `Car with ${v.doors} doors`;
}
}
Intersection Types
Combine multiple types into one:
type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged;
const employee: Person = { name: "Diana", age: 28 };
Type Aliases
Create a new name for any type:
type StringOrArray = string | string[];
Functions
aboolean optional, default, rest parameters, and overloading.
// Optional parameter
function buildName(first: string, last?: string): string {
return last ? `${first} ${last}` : first;
}
// Default value
function multiply(a: number, b: number = 1): number {
return a * b;
}
// Rest parameter
function sum(...numbers: number[]): number {
return numbers.reduce((total, n) => total + n, 0);
}
// Overload signatures
function combine(a: number, b: number): number;
function combine(a: string, b: string): string;
function combine(a: any, b: any): any {
return a + b;
}
Interfaces and Type Comparison
Interfaces describe object shapes:
interface Book {
title: string;
author: string;
pages?: number;
readonly isbn: string;
}
alen arbitrary properties via index signatures:
interface Config {
[key: string]: string | number;
}
Interface vs type alias
- Both can describe objects and function signatures.
- Type aliases support unions, tuples, and primitives; interfaces do not.
- Interfaces can be merged (declaration merging); type aliases cannot.
- Classes can implement both, but not a type alias represanting a union.
// Merging interfaces
interface Window {
title: string;
}
interface Window {
ts: number;
}
// Result: { title: string; ts: number }
Classes
Classes suport fields, constructors, and methods:
class Account {
private _balance: number;
readonly owner: string;
constructor(owner: string, initialBalance: number) {
this.owner = owner;
this._balance = initialBalance;
}
get balance(): number {
return this._balance;
}
deposit(amount: number): void {
this._balance += amount;
}
}
ECMAScript private fields (#) provide hard privacy:
class Secret {
#key: string;
constructor(key: string) {
this.#key = key;
}
}
Abstract classes require subclasses to implement abstract methods:
abstract class Shape {
abstract getArea(): number;
}
class Circle extends Shape {
constructor(private radius: number) {
super();
}
getArea(): number {
return Math.PI * this.radius ** 2;
}
}
Method overloading inside a class follows the same pattern as function overloading.
Generics
Generics enable reusable components that work with many types.
Generic Functions
function identity<T>(value: T): T {
return value;
}
const num = identity<number>(42);
const str = identity("hello"); // inferred
Generic Constraints
Restrict generic types using extends:
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(item: T): T {
console.log(item.length);
return item;
}
Generic Interfaces and Classes
interface Repository<T> {
getById(id: string): T;
getAll(): T[];
}
class DataStore<T> {
private items: T[] = [];
add(item: T) {
this.items.push(item);
}
}
Built‑in Utility Types
TypeScript ships with many helper types built on generics.
Partial<T>– makes all properties optional.Required<T>– makes all properties required.Readonly<T>– makes properties read‑only.Pick<T, K>– selects a subset of properties.Record<K, V>– constructs an object type with keys of type K and values of type V.Exclude<T, U>/Extract<T, U>– filter union members.ReturnType<T>– obtains the return type of a function.
Encoding these relies on keyof, in, and infer:
type MyPartial<T> = {
[P in keyof T]?: T[P];
};
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
Decorators
Decorators are functions that modify classes, methods, properties, or parameters. Enable them with experimentalDecorators in tsconfig.json.
Class Decorator
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class User {
constructor(public name: string) {}
}
Property Decorator
function uppercase(target: any, propertyKey: string) {
let value: string;
const getter = () => value;
const setter = (newVal: string) => {
value = newVal.toUpperCase();
};
Object.defineProperty(target, propertyKey, { get: getter, set: setter });
}
class Greeter {
@uppercase
greeting: string;
constructor(message: string) {
this.greeting = message;
}
}
Method Decorator
function measure(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
const start = performance.now();
const result = original.apply(this, args);
const end = performance.now();
console.log(`${propertyKey} took ${end - start}ms`);
return result;
};
}
class Calculator {
@measure
heavyComputation() {
// simulate work
for (let i = 0; i < 1e6; i++) {}
}
}
Parameter Decorator
function required(target: any, propertyKey: string, parameterIndex: number) {
console.log(`Parameter at index ${parameterIndex} in ${propertyKey} is required`);
}
class Service {
execute(@required task: string) {
// ...
}
}
Recent TypeScript Improvements
Class field inference from constructors (TypeScript 4.0) detects property types even without explicit annotations when noImplicitAny is on. For example, a field assigned only a string will be inferred as string.
Labeled tuple elements improve readability:
type Address = [street: string, city: string, zip: number];
function getLocation(...[street, city, zip]: Address) {
console.log(`${street}, ${city} ${zip}`);
}
Project Configuration
The tsconfig.json file defines the compiler options and root files. Common settings:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"strict": true,
"outDir": "./dist",
"rootDir": "./src",
"experimentalDecorators": true
},
"include": ["src/**/*"]
}
Key options include strict (enables all strict checks), paths (module aliases), and lib (standard library declarations).
Development Tooling
- The TypeScript Playground provides an interactive environment to test code and see compiled output.
- TypeDoc generates documentation from source comments.
- typescript-eslint enforces coding standards and consistency.
- JSON to TS converters and Schemats help generate type definitions from data.
- AST viewers like TypeScript AST Viewer visualise the abstract syntax tree.