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"]
}
Defence Preparation
Be prepared to explain topics like these:
- Why use TypeScript instead of plain JavaScript? What problems does it solve? — TypeScript adds static type checking, catching type-related bugs at compile time instead of at runtime. It provides better IDE support with autocompletion, refactoring tools, and inline documentation. It makes code more self-documenting because function signatures show what types are expected and returned. The type system also helps when working in teams, because interfaces define clear contracts between different parts of the codebase.
- What is the difference between
anyandunknown? Why isunknownsafer? —anydisables all type checking — you can assign it to anything and call any method on it without errors.unknownis the type-safe counterpart: you can assign any value to anunknownvariable, but you cannot use it without first narrowing its type (viatypeof,instanceof, or a type guard). Useunknownwhen you receive data of uncertain type (e.g., from an API) and want to enforce proper validation before use. - What is the difference between
interfaceandtypealias? When would you choose one over the other? — Both can describe object shapes, function signatures, and be extended. Interfaces support declaration merging (defining the same interface twice merges the definitions) and are the idiomatic choice for object shapes and class contracts. Type aliases support union types (string | number), intersection types, mapped types, and conditional types. Useinterfacefor public APIs and object shapes; usetypefor unions, complex type transformations, and when you need features interfaces don't support. - How do union types work, and how do you narrow them safely? — A union type (
string | number) means the value can be any of the listed types. To use type-specific methods, you must narrow the type usingtypeofchecks,instanceof, equality checks, or theinoperator. TypeScript's control flow analysis understands these guards — inside anif (typeof x === 'string')block,xis treated asstring. Discriminated unions use a shared literal property (e.g.,type: 'circle' | 'square') for exhaustive pattern matching withswitch. - What are generics, and why are they useful? Give an example. — Generics let you write functions, classes, or interfaces that work with any type while preserving type safety. Instead of using
any, you define a type parameter<T>that is determined when the function is called. Example:function first<T>(arr: T[]): T | undefined { return arr[0]; }— callingfirst([1, 2, 3])infersTasnumberand the return type asnumber | undefined. Generic constraints (<T extends HasId>) restrict which types are accepted. - What is
keyofand how does indexed access work? —keyof Tproduces a union of all property names (keys) of typeTas string literals.T[K](indexed access) gives you the type of propertyKonT. Together they enable type-safe property access:function getProperty<T, K extends keyof T>(obj: T, key: K): T[K]— the compiler knows the return type based on which key you pass. This prevents accessing non-existent properties and provides correct return type inference. - Name three TypeScript utility types and explain what they do. —
Partial<T>makes all properties ofToptional, useful for update functions where you only provide changed fields.Pick<T, Keys>creates a type with only the specified properties fromT, useful for selecting a subset of a larger interface.Omit<T, Keys>creates a type with all properties except the specified ones, useful for excluding sensitive or internal fields. Others includeRequired<T>,Record<K, V>, andReadonly<T>. - What are discriminated unions (tagged unions), and why are they useful for TypeScript applications? — A discriminated union is a union of object types that share a common literal property (the "discriminant"), such as
type: 'success' | 'error'. TypeScript can narrow the union based on this property inswitchorifstatements, giving you access to type-specific properties in each branch. They are useful for modeling states (loading/success/error), API responses, and any scenario where an object can be one of several shapes with different data.