TypeScript

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
anywhen 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
undefinedandnullrespectively. - With
strictmode (recommended), null and undefined are only assignable toany,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
nevertype represents the type of values that never occur. - Return type for functions that always throw or never return.
- Variables acquire the type
neverwhen 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
nullorundefined. - 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
interfacefor object shapes — they can be extended and implemented by classes - Use
typefor 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
readonlybefore 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 usereadonly.
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,
thisis 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
instanceofneeds 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"]
}