Skip to main content

TypeScript

TS

Why TypeScript?

  • Superset of JavaScript — all valid JS is valid TS
  • Adds static type checking at compile time — catch bugs before runtime
  • Better IDE support — autocompletion, refactoring, navigation
  • Self-documenting code — types serve as documentation
  • Compiles to plain JavaScript — types are erased at runtime

Setup — Vite

npm create vite@latest my-ts-app -- --template vanilla-ts

cd my-ts-app
npm install
npm run dev

code .

Write some code and then inspect the build result:

npm run build

::: tip Historically Webpack and Babel were used to compile TypeScript. Vite is the modern standard — faster, simpler configuration. :::

Type Checking

OK in pure JS — no errors, but unexpected results.

function add(a, b) {
return a + b;
}

let ab = add("a", 5); // "a5" — string concatenation, not addition

TS catches multiple errors at compile time:

function add(a: number, b: number): string {
return a + b; // Error: Type 'number' is not assignable to type 'string'
}

let sum = 5;
sum = add("a", 5); // Error: Argument of type 'string' is not assignable to parameter of type 'number'

Full types defined — no errors:

function add(a: number, b: number): string {
return (a + b).toString();
}
let sum: string = "5";
sum = add(1, 5);

Or using type inference — TS infers return type as string and sum as string:

function add(a: number, b: number) {
return (a + b).toString();
}
let sum = "5";
sum = add(1, 5);

Basic Types

boolean, number, string, string interpolation

let isDone: boolean = false;

let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
let octal: number = 0o744;

let fullName: string = `Andres Käver`;
let born: number = 1974;
let sentence1: string = `Hello, my name is ${fullName}.

I am ${2020 - born} years old.`;

let sentence2: string = "Hello, my name is " + fullName + ".\n\n" + "I am " + (2020 - born) + " years old.";

Basic Types – Array, Tuple

let list1: number[] = [1, 2, 3];
let list2: Array<number> = [1, 2, 3];

// Declare a tuple type
let x: [string, number];
// Initialize it
x = ["akaver", 1974]; // OK

// Error: Type 'number' is not assignable to type 'string'
x = [1974, "akaver"];

console.log(x[0].substring(1)); // OK

// Error: Property 'substring' does not exist on type 'number'
console.log(x[1].substring(1));

// Error: Tuple type '[string, number]' of length '2' has no element at index '3'
x[3] = "world";

Enum

  • Fixed set of values (typically numbers)
  • Values start from 0, can be specified
  • Provides mapping in both directions
  • String enums
    • No reverse mapping
enum Color1 {
Red,
Green,
Blue,
}
let c1: Color1 = Color1.Green;

enum Color2 {
Red = 1,
Green = 2,
Blue = 4,
}
let c2: Color2 = Color2.Green;

// enum reverse mapping
let colorName: string = Color1[c1];
let colorCode: number = Color1["Red"];

enum Names {
foo = "Foo",
bar = "Bar",
}
let n: Names = Names.foo;

Union Types

If a variable/parameter must accept several possible types — use union types with |.

let s: string | null | undefined = "foo";
s = null;
s = undefined;
s = 5; // Error: Type 'number' is not assignable to type 'string | null | undefined'

Union types are foundational in TS — used everywhere for nullable values, function overloads, and state modeling.

function formatInput(input: string | number): string {
if (typeof input === "string") {
return input.toUpperCase(); // TS knows input is string here
}
return input.toFixed(2); // TS knows input is number here
}

Literal Types

Allows using only predefined values. Combines naturally with union types.

type Easing = "ease-in" | "ease-out" | "ease-in-out";
type Duration = 100 | 200 | 400;

function animate(easing: Easing, duration: Duration) {
// only specific values allowed
}
animate("ease-in", 200); // OK
animate("linear", 300); // Error on both arguments

Any

  • Opt-out of type checking and let the values pass through compile-time checks.
  • Return to plain JS approach — type is associated with value. No type checking by TS compiler.
  • Useful when working with external sources (json, html, libraries)
let notSure: any = 4;

// ifItExists might exist at runtime
notSure.ifItExists();

// toFixed exists (no compiler check)
notSure.toFixed();

notSure = "maybe a string instead";
// definitely a boolean
notSure = false;

let list: any[] = [1, true, "free"];

Unknown

  • Safer alternative to any — you must narrow the type before using it.
  • Preferred over any when the type is genuinely unknown (API responses, JSON parsing, error handling).
let value: unknown = "hello";

// Error: Object is of type 'unknown' — cannot use directly
value.toUpperCase();

// Must narrow the type first
if (typeof value === "string") {
value.toUpperCase(); // OK — TS knows it's a string
}

// Common pattern: error handling
try {
// ...
} catch (e: unknown) {
if (e instanceof Error) {
console.log(e.message); // OK — narrowed to Error
}
}

Void

Opposite of any. Usually used as return type from functions that don't return anything.

function testVoid(): void {
console.log("foobar");
// only undefined is allowed
return undefined;
}

let voidResult = testVoid();
console.log(voidResult); // undefined
voidResult = undefined;
voidResult = null; // Error: Type 'null' is not assignable to type 'void'

null, undefined

  • In TypeScript, both undefined and null actually have their own types named undefined and null respectively.
  • With strict mode (recommended), null and undefined are only assignable to any, unknown, and their respective types (the one exception being that undefined is also assignable to void).
  • Use union types to explicitly allow null/undefined: string | null

never

  • The never type represents the type of values that never occur.
  • Return type for functions that always throw or never return.
  • Variables acquire the type never when narrowed by type guards that can never be true.
// Function returning never must have unreachable end point
function error(message: string): never {
throw new Error(message);
}

// Inferred return type is never
function fail() {
return error("Something failed");
}

// Function returning never must have unreachable end point
function infiniteLoop(): never {
while (true) {}
}

object

object is a type that represents the non-primitive type, i.e. anything that is not number, string, boolean, bigint, symbol, null, or undefined.

declare function create(o: object | null): void;
create({ prop: 0 }); // OK
create(null); // OK
create(42); // Error
create("string"); // Error
create(false); // Error
create(undefined); // Error

Type Assertions (Casting)

  • Sometimes you know more than TS compiler. :-)
  • Type assertions are a way to tell the compiler "trust me, I know what I'm doing." A type assertion is like a type cast in other languages, but performs no special checking or restructuring of data. It has no runtime impact, and is used purely by the compiler.
  • as-style is the only one supported in JSX/TSX.
let someValue: any = "this is a string";
let strLength1: number = (<string>someValue).length;
let strLength2: number = (someValue as string).length; // preferred

Non-null Assertion Operator !

  • Tells the compiler that a value is definitely not null or undefined.
  • Use when you are certain a value exists but TS cannot prove it.
  • Use sparingly — it bypasses safety checks.
let element = document.getElementById("app"); // Type: HTMLElement | null

// Error: Object is possibly 'null'
element.innerHTML = "hello";

// Non-null assertion — you guarantee it exists
element!.innerHTML = "hello";

// Safer alternative — check first
if (element) {
element.innerHTML = "hello"; // TS narrows to HTMLElement
}

Type Aliases

Type aliases create a new name for a type. Type aliases are sometimes similar to interfaces, but can name primitives, unions, tuples, and any other types.

type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;

function getName(n: NameOrResolver): Name {
if (typeof n === "string") {
return n;
} else {
return n();
}
}

When to use type vs interface:

  • Use interface for object shapes — they can be extended and implemented by classes
  • Use type for unions, primitives, tuples, and complex type compositions

Interfaces

  • TS type checking focuses on the shape that values have. This is sometimes called "duck typing" or "structural subtyping".
  • In TS, interfaces fill the role of naming these types, and are a powerful way of defining contracts within your code as well as contracts with code outside of your project.
interface Person {
name: string;
}
function printLabel(p: Person) {
console.log(p.name);
}
let myPerson = { name: "akaver", age: 10 };
printLabel(myPerson);

Interfaces — Optional and Readonly Properties

  • Not all properties of an interface may be required. Some exist under certain conditions or may not be there at all.
  • Optional property — denoted by a ? at the end of the property name.
  • Readonly property — put readonly before the property name. Can only be set when the object is first created.
  • TypeScript comes with a ReadonlyArray<T> type with all mutating methods removed.
  • Rule of thumb: variables use const, properties use readonly.

Interfaces — Excess Property Checks

  • Object literals get special treatment and undergo excess property checking when assigning them to other variables, or passing them as arguments.
  • If an object literal has any properties that the "target type" doesn't have — error.
  • Use index signature to allow unknown properties.
interface Person {
readonly name: string;
age?: number;
[propName: string]: any;
}

function printLabel(p: Person) {
console.log(p.name);
}

printLabel({ name: "akaver", foo: "Bar" });

Interfaces — Function Types

  • To describe a function type within an interface, give the interface a call signature.
  • This is like a function declaration with only the parameter list and return type given.
  • Each parameter in the parameter list requires both name and type.
interface SearchFunc {
(source: string, subString: string): boolean;
}

Classes

  • Traditional JS does not have classes — they were introduced with ES6 (2015). TS has always supported them.
  • Few changes compared to ES6
    • Declare basic properties
    • Public, private (and JS # private) and protected modifiers

Classes – public, private, protected

  • Public by default.
  • Private — cannot be accessed outside from its containing class.
  • The protected modifier acts much like the private modifier with the exception that members declared protected can also be accessed within deriving classes.
  • TS is a structural type system. Comparing two different types, regardless of where they came from, if the types of all members are compatible — types themselves are compatible.
  • Comparing types that have private and protected members — for types to be considered compatible private members have to be originated in the same declaration. The same applies to protected members.

Classes – readonly, parameter properties

  • Properties can be made readonly by using the readonly keyword.
  • Readonly properties must be initialized at their declaration or in the constructor.
  • Parameter properties let you create and initialize a member in one place.
  • Parameter properties are declared by prefixing a constructor parameter with an accessibility modifier or readonly, or both.
class Point {
private x: number;
private y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}

// vs Parameter properties — much shorter

class Point {
constructor(
public x: number,
public y: number
) {}
}

Classes – Static, Abstract

  • Static members are visible on the class itself rather than on instances.
  • Each instance accesses this value through prepending the name of the class.
  • Abstract classes are base classes from which other classes may be derived. They may not be instantiated directly.
  • Unlike an interface, an abstract class may contain implementation details for its members.
abstract class Shape {
abstract area(): number; // must be implemented by subclasses

describe(): string {
return `This shape has area ${this.area()}`;
}
}

class Circle extends Shape {
constructor(private radius: number) {
super();
}

area(): number {
return Math.PI * this.radius ** 2;
}
}

// const s = new Shape(); // Error: Cannot create an instance of an abstract class
const c = new Circle(5);
console.log(c.describe()); // "This shape has area 78.53..."

Interfaces — Class Implementation

  • One of the most common uses of interfaces — explicitly enforcing that a class meets a particular contract.
  • Like classes, interfaces can extend each other.
  • An interface can extend multiple interfaces.
interface ClockInterface {
currentTime: Date;
setTime(d: Date): void;
}

class Clock implements ClockInterface {
currentTime: Date = new Date();
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) {}
}

Functions

Function type:

let myAdd: (baseValue: number, increment: number) => number
= function (x: number, y: number): number
{
return x + y;
};
  • Optional and Default Parameters
    • TS requires that parameter types and their count match (not like JS)
  • Optional parameter — add ? at the end of parameter name.
  • Any optional parameters must follow required parameters.
  • Default value that a parameter will be assigned if the user does not provide one, or if the user passes undefined in its place — default-initialized parameters.
  • Default-initialized parameters that come after all required parameters are treated as optional

Functions – this parameter

  • In JavaScript, this is a variable that's set when a function is called. This makes it a very powerful and flexible feature, but it comes at the cost of always having to know about the context that a function is executing in.
  • Provide an explicit this parameter. this parameters are fake parameters that come first in the parameter list of a function.
function numberPressed(this: GlobalEventHandlers, event: Event) {
let elem = this as HTMLAnchorElement; // type assertion
let key = elem.dataset.value;
display!.innerHTML = calcBrain.handleKey(key!); // non-null assertions
}

Generics

Generics are similar to C# generics — create reusable components that work with multiple types.

function identity<T>(arg: T): T {
return arg;
}

let output1 = identity<string>("hello"); // explicit type
let output2 = identity(42); // type inferred as number

Generic Constraints

Use extends to constrain what types are allowed:

interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
loggingIdentity({ length: 10, value: 3 }); // OK — has length
loggingIdentity(42); // Error: number doesn't have .length

Advanced Types — Intersection

Intersection types glue types together — the result has all properties from both types.

interface HasName {
name: string;
}
interface HasAge {
age: number;
}

type Person = HasName & HasAge;

let p: Person = { name: "akaver", age: 30 }; // must have both

Advanced Types — Type Guards

When working with union types, you need to differentiate between types. Repeated type assertions are not ideal:

interface Bird {
fly(): any;
layEggs(): any;
}
interface Fish {
swim(): any;
layEggs(): any;
}
function getSmallPet(): Fish | Bird {
//...
}
let pet = getSmallPet();
if ((pet as Fish).swim) {
(pet as Fish).swim();
} else if ((pet as Bird).fly) {
(pet as Bird).fly();
}

Using type predicates

Define a custom type guard function with is keyword:

function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}

if (isFish(pet)) {
pet.swim(); // TS knows pet is Fish
} else {
pet.fly(); // TS knows pet is Bird
}

typeof type guards

TS recognizes typeof checks and narrows types automatically:

  • typeof v === "typename" where typename is "number", "string", "boolean", or "symbol"

instanceof type guards

  • The right side of instanceof needs to be a constructor function
  • TS will narrow down to the type of that constructor's prototype

Discriminated Unions (Tagged Unions)

A common and powerful pattern — use a shared literal property to distinguish between union members. TS narrows the type based on the discriminant.

interface Circle {
kind: "circle";
radius: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
type Shape = Circle | Rectangle;

function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2; // TS knows it's Circle
case "rectangle":
return shape.width * shape.height; // TS knows it's Rectangle
}
}

This pattern is used everywhere in frameworks — Redux actions, API response states, component variants.

keyof and Indexed Access Types

keyof produces a union of an object type's property names:

interface Person {
name: string;
age: number;
}

type PersonKeys = keyof Person; // "name" | "age"

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}

let person: Person = { name: "akaver", age: 30 };
let name = getProperty(person, "name"); // type: string
let age = getProperty(person, "age"); // type: number
getProperty(person, "email"); // Error: "email" is not in keyof Person

as const Assertions

as const makes values deeply readonly and narrows types to their literal values:

// Without as const — type is string[]
const colors = ["red", "green", "blue"];

// With as const — type is readonly ["red", "green", "blue"]
const colors = ["red", "green", "blue"] as const;

// Useful for configuration objects
const config = {
api: "https://api.example.com",
timeout: 5000,
} as const;
// config.api is type "https://api.example.com", not string
// config.timeout is type 5000, not number

// Common pattern: deriving types from values
const ROLES = ["admin", "user", "guest"] as const;
type Role = (typeof ROLES)[number]; // "admin" | "user" | "guest"

Utility Types

TypeScript provides built-in utility types for common type transformations. These are used heavily in framework code.

interface User {
id: number;
name: string;
email: string;
age: number;
}

// Partial<T> — all properties become optional
function updateUser(id: number, updates: Partial<User>) {
// updates can have any subset of User properties
}
updateUser(1, { name: "new name" }); // OK — only name provided

// Required<T> — all properties become required (opposite of Partial)

// Pick<T, K> — select only certain properties
type UserPreview = Pick<User, "id" | "name">;
// { id: number; name: string }

// Omit<T, K> — remove certain properties
type UserWithoutEmail = Omit<User, "email">;
// { id: number; name: string; age: number }

// Record<K, V> — construct an object type with keys K and values V
type RolePermissions = Record<"admin" | "user" | "guest", boolean>;
// { admin: boolean; user: boolean; guest: boolean }

Declaration Files .d.ts

  • When using JavaScript libraries in TypeScript, you need type definitions.
  • Declaration files (.d.ts) provide type information without implementation.
  • Many popular libraries ship their own types. For others, install from DefinitelyTyped:
npm install --save-dev @types/lodash
npm install --save-dev @types/node
  • If no types exist, you can declare the module to suppress errors:
// global.d.ts
declare module "some-untyped-library";

tsconfig.json

Typical options for TS:

{
"compilerOptions": {
"module": "esnext",
"target": "es2017",
"strict": true,
"alwaysStrict": true,
"noImplicitAny": true,
"noImplicitThis": true,
"sourceMap": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"removeComments": true,
"moduleResolution": "node",
"lib": ["ES5", "ES6", "ES7", "ES2017", "ES2018", "ES2019", "ES2020", "ESNext", "DOM", "DOM.Iterable"]
},
"include": ["global.d.ts", "./src/**/*"],
"exclude": ["node_modules"]
}