3TypeScript Crash Course
What you will master here
- Primitives, arrays, tuples, objects
- type vs interface — when to pick which
- Union, intersection, literal types
- Generics — functions and types
- Narrowing: typeof, in, instanceof, type predicates
- Utility types: Partial, Required, Pick, Omit, Record, Readonly, ReturnType, Awaited
- Enums, decorators, namespaces
- tsconfig essentials and strict mode
3.1 Why TypeScript
JS at runtime is untyped — bugs surface only when the bad code runs. TS adds compile-time type checking. Same JS output, with safety + IDE auto-complete + safer refactors. SDET-relevant payoff: fewer "is undefined" failures in test code, easier to evolve a Page Object Model.
3.2 Basic types
// Primitives
const name: string = 'Alice';
const age: number = 30;
const isAdmin: boolean = true;
const huge: bigint = 9007199254740993n;
const sym: symbol = Symbol('id');
const u: undefined = undefined;
const n: null = null;
// Special
const x: any = anything; // opt-out — avoid
const y: unknown = anything; // safe any — must narrow before use
const z: never = (() => { throw new Error(); })(); // never returns
function log(msg: string): void {} // returns nothing
// Arrays + tuples
const nums: number[] = [1,2,3];
const nums2: Array<number> = [1,2,3];
const pair: [string, number] = ['Alice', 30]; // fixed length + types
// Object
const user: { name: string; age?: number } = { name: 'A' }; // age optional
3.3 type vs interface
interface
interface User {
name: string;
age: number;
}
interface User { email: string; } // declaration merging — adds email
interface Admin extends User { role: 'admin' }
Open for extension (declaration merging). Best for public API shapes.
type
type User = { name: string; age: number };
type Id = string | number; // unions
type Pair<T> = [T, T]; // generic alias
type Coord = { x: number } & { y: number }; // intersection
More flexible (unions, intersections, primitives). Best for utilities and combinations.
Pragmatic ruleUse interface for object shapes your team extends. Use type for unions, intersections, and computed types. Either works for the rest.
3.4 Union and intersection
// Union — A or B
type Result = { ok: true; value: string } | { ok: false; error: string };
function handle(r: Result) {
if (r.ok) console.log(r.value); // narrowed to success
else console.log(r.error); // narrowed to failure
}
// Intersection — A and B
type WithTimestamp = { createdAt: Date };
type UserWithTs = User & WithTimestamp;
// must have name, age, createdAt
3.5 Literal types
type Role = 'admin' | 'user' | 'guest'; // string literal union
type Dir = 'N' | 'S' | 'E' | 'W';
type Bit = 0 | 1;
function setRole(r: Role) { /* ... */ }
setRole('admin'); // OK
setRole('superuser'); // type error
3.6 Generics
Type parameters let a function/class work over many concrete types while preserving information.
function first<T>(arr: T[]): T | undefined { return arr[0]; }
const a = first([1,2,3]); // a: number | undefined
const b = first(['x','y']); // b: string | undefined
// Multiple type params + constraints
function pluck<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const u = { name: 'A', age: 30 };
pluck(u, 'age'); // number
pluck(u, 'foo'); // ERROR — 'foo' not in keys of u
// Generic interface
interface Box<T> { value: T }
const numBox: Box<number> = { value: 1 };
// Generic class
class Stack<T> {
private items: T[] = [];
push(x: T) { this.items.push(x); }
pop(): T | undefined { return this.items.pop(); }
}
3.7 Narrowing
function fmt(x: string | number) {
if (typeof x === 'string') return x.toUpperCase(); // narrowed: string
return x.toFixed(2); // narrowed: number
}
function area(s: { kind: 'circle', r: number } | { kind: 'square', s: number }) {
switch (s.kind) {
case 'circle': return Math.PI * s.r ** 2; // narrowed: circle
case 'square': return s.s ** 2;
}
}
// in narrowing
type Cat = { meow: () => void };
type Dog = { bark: () => void };
function speak(p: Cat | Dog) {
if ('meow' in p) p.meow();
else p.bark();
}
// User-defined type predicate
function isString(v: unknown): v is string {
return typeof v === 'string';
}
function up(v: unknown) {
if (isString(v)) return v.toUpperCase();
}
3.8 Utility types — the ones you'll use
interface User { id: string; name: string; age: number }
type Partial1 = Partial<User>; // all fields optional
type Required1 = Required<User>; // all fields required
type ReadOnly1 = Readonly<User>; // can't reassign fields
type Pick1 = Pick<User, 'id' | 'name'>; // only id + name
type Omit1 = Omit<User, 'age'>; // everything except age
type Record1 = Record<string, User>; // { [k: string]: User }
type NonNull = NonNullable<string | null>; // string
type RT = ReturnType<typeof fn>; // type the fn returns
type Aw = Awaited<Promise<string>>; // string (unwraps promise)
type Params = Parameters<typeof fn>; // tuple of fn's params
function fn(a: number, b: string): boolean { return true; }
// RT = boolean, Params = [number, string]
3.9 Enums (and why as const often beats them)
// Classic enum
enum Status { Pending = 'pending', Paid = 'paid', Failed = 'failed' }
const s: Status = Status.Pending;
// Modern alternative — object + literal union (smaller output, simpler)
const STATUS = { Pending: 'pending', Paid: 'paid', Failed: 'failed' } as const;
type Status2 = typeof STATUS[keyof typeof STATUS]; // 'pending' | 'paid' | 'failed'
TipModern TS code often skips enums in favour of
as const objects. Smaller runtime, easier to refactor, same type safety.3.10 Mapped & conditional types
// Mapped — transform each property
type Stringify<T> = { [K in keyof T]: string };
type S = Stringify<User>; // { id: string; name: string; age: string }
// Conditional — ternary in the type system
type IsString<T> = T extends string ? 'yes' : 'no';
type A = IsString<'hi'>; // 'yes'
type B = IsString<42>; // 'no'
// Extract / Exclude
type Pets = 'cat' | 'dog' | 'fish';
type Furry = Extract<Pets, 'cat' | 'dog'>; // 'cat' | 'dog'
type NotFish = Exclude<Pets, 'fish'>; // 'cat' | 'dog'
3.11 Classes — TS additions
class Account {
// Access modifiers
public id: string;
private balance: number;
protected currency = 'USD';
readonly createdAt = new Date();
// Param properties — shorthand
constructor(id: string, balance: number) {
this.id = id; this.balance = balance;
}
/* equivalent shorthand:
constructor(public id: string, private balance: number) {}
*/
// Method
deposit(amount: number): void { this.balance += amount; }
// Getter
get summary() { return `${this.id}: ${this.balance}`; }
}
abstract class Shape {
abstract area(): number; // subclasses must implement
}
3.12 tsconfig.json — what matters
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true, // enable ALL strict checks
"noImplicitAny": true, // (included in strict)
"strictNullChecks": true,
"noUncheckedIndexedAccess": true, // arr[i] is T | undefined
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"isolatedModules": true,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Always strictTurn on
strict: true from day one. Retrofitting later is painful. Add noUncheckedIndexedAccess if you can — catches a lot of "undefined" bugs in test fixtures.3.13 Common TS patterns in test code
// Discriminated union for response handling
type ApiResult<T> =
| { ok: true; data: T }
| { ok: false; error: { code: number; message: string } };
async function fetchUser(id: string): Promise<ApiResult<User>> {
try {
const r = await fetch(`/users/${id}`);
if (!r.ok) return { ok: false, error: { code: r.status, message: r.statusText } };
return { ok: true, data: await r.json() };
} catch (e: any) {
return { ok: false, error: { code: 0, message: e.message } };
}
}
// Page Object generic
interface PageObject {
page: Page;
}
// Type-safe fixtures
type Fixtures = {
loginPage: LoginPage;
authedUser: { id: string; email: string };
};
const test = base.extend<Fixtures>({ /* ... */ });
Module 19 — TS interview Q&A bank
What does TypeScript give you over JS?
Static type checking at build time — catches "is undefined" / wrong-shape errors before runtime, enables safer refactors and auto-complete. Output is plain JS; types disappear at runtime.
Difference between any and unknown?
any disables type checking — anything goes. unknown is the safe alternative — you must narrow it (typeof / type predicate) before using. Always prefer unknown over any for inputs you don't trust.
type vs interface?
Both describe object shapes. interface supports declaration merging and is conventional for public API shapes. type can be aliased to unions, intersections, tuples, primitives — more flexible. Pick interface for extensible shapes, type for unions/utilities.
What's a generic?
A type parameter that lets a function/class operate on many concrete types while preserving the relationship:
function first<T>(arr: T[]): T | undefined. Caller passes T implicitly via the argument.What is narrowing?
When the compiler refines a union type based on a runtime check. Examples: typeof, in, instanceof, equality with literal, user-defined predicate
x is T. Inside the branch, the variable has the narrower type.What is a type predicate (user-defined type guard)?
A function that returns a boolean and tells the compiler "if this is true, the parameter is type X". Signature:
function isString(v: unknown): v is string. Caller code uses it to narrow.What are utility types you use most?
Partial (all optional), Pick (subset of fields), Omit (everything except listed), Record (object of fixed key-type to value-type), ReturnType (type of a function's return), Awaited (unwrap a Promise type), NonNullable (remove null/undefined).
What's a discriminated union?
A union whose members share a literal-typed "tag" field. Switching on the tag narrows perfectly. Used for result types (
{ ok: true, value } | { ok: false, error }), event types, state machines.Why prefer as const objects over enums?
Enums emit runtime code (an object) and have quirks (numeric enums are reverse-mapped).
as const objects with a derived union type give same safety, less output, simpler refactoring.What does keyof do?
Produces a union of the keys of a type.
keyof { a: 1; b: 2 } is 'a' | 'b'. Combine with generics: K extends keyof T for safe property access.What's a mapped type?
A type that iterates the keys of another type and transforms each property — like
{ [K in keyof T]: ... }. Used to build utility types (Partial, Readonly) and DTOs.What's a conditional type?
A ternary in the type system:
T extends U ? X : Y. Combined with infer it can extract types (e.g. ReturnType uses it). Powerful but easy to over-use.What does strict mode enable?
noImplicitAny, strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, alwaysStrict, noImplicitThis, useUnknownInCatchVariables. Catches the most common JS bug classes at compile time.
What is noUncheckedIndexedAccess?
Optional strict flag. Makes
arr[i] return T | undefined instead of T. Forces you to handle out-of-bounds. Catches a huge class of "cannot read undefined" bugs.How do you type an async function's return?
Promise<T>. If you want to extract T from a Promise type, use Awaited<Promise<T>> = T.What's an abstract class?
A class that can't be instantiated directly and may declare abstract methods subclasses must implement. Useful as a base class with shared behaviour + required hooks. Common in test framework BasePage patterns.
How do you make readonly properties?
readonly on class fields, or Readonly<T> at the type level, or as const on a literal. Prevents reassignment; doesn't prevent deep mutation.What's the difference between satisfies and as?
as T is a type assertion — trust me, treat this as T (no check). satisfies T (TS 4.9+) checks that the value matches T but keeps the inferred narrower type. Prefer satisfies when you can.3.14 More TS interview Q&A
What's a conditional type?
Ternary at type level:
T extends U ? X : Y. Powers utility types like ReturnType, Awaited. Distributive over unions: T extends any ? T[] : never applies branch to each member.What does infer do?
Pattern-matches and captures a type within a conditional.
T extends (...args: any[]) => infer R ? R : never = "if T is a function, capture its return type as R". Foundation of many utility types.What's a mapped type?
{ [K in keyof T]: ... } iterates keys and transforms property types. Used to build Partial, Readonly, Pick, custom DTOs. Modifiers: +/-readonly, +/-?.What's a template literal type?
type Route = `/${string}`. Strings as types with placeholders. Powers type-safe URLs, event-name conventions, CSS prop types.What's a discriminated union?
Union of object types sharing a literal-typed "tag" field. Narrowing on the tag (in switch) gives exhaustive type-safe handling:
case 'login': // type is the login variant.What's a branded type?
Nominal typing in TS's structural system.
type UserId = string & { __brand: 'UserId' }. Prevents passing a raw string or different branded id. Compile-time only — no runtime cost.What is exhaustive checking with never?
In default branch of a switch on a discriminated union:
const _: never = x. If union grows and you forget a case, TS errors on the never assignment.Difference between interface extending and intersection?
Interface extends: nominal extension, can fail on incompatible members. Intersection (
A & B): structural merge, conflicting members become never. Interfaces also support declaration merging.What's the difference between type and interface in modern TS?
Mostly interchangeable for objects. Interface allows declaration merging. Type allows unions, intersections, primitives, mapped types. Use type for utility/computed types; interface for public object shapes.
What's the unknown type vs any?
any disables type checking. unknown is safe — you can assign anything to it, but must narrow before using. Always prefer unknown for inputs you don't trust.
What does declare do?
Ambient declarations — tell TS "this exists somewhere, trust me". Used to type third-party JS libraries without runtime code, or globals (
declare global { interface Window { __pwgReady: boolean } }).What's tsconfig.json path mapping for?
Alias paths to avoid relative-import hell.
"paths": { "@/*": ["./src/*"] } lets you write import x from '@/utils/x'. Bundler must respect the alias too.How do you type a function with optional or default args?
Optional:
fn(x: string, y?: number). Default: fn(x: string, y = 0) (TS infers y as number). Optional + default are the same at the call site.What's a recursive type?
A type that references itself:
type Json = string | number | boolean | null | Json[] | { [k: string]: Json }. Useful for tree structures, AST nodes.What's as const?
Tells TS to infer the narrowest possible type — literal types, readonly arrays/objects.
const x = ['a','b'] as const → type is readonly ['a','b'], not string[].How do you express "either X or Y but not both" at the type level?
XOR via conditional:
type XOR<A,B> = (A & { [K in keyof B]?: never }) | (B & { [K in keyof A]?: never }). Common for "one of these props required" component prop types.What's the TypeScript compiler output mode?
target sets the JS version (ES2015, ES2022, ESNext). module sets the module system (CommonJS, ESNext, Node16). moduleResolution tells how to resolve imports (Node, Bundler).Why might TS compile fail in CI but pass locally?
Different TS version, different tsconfig (CI uses tsconfig.build.json with stricter flags), different OS file-system case sensitivity (case-insensitive on Mac, case-sensitive on Linux), uncommitted .d.ts changes. Pin TS version + run
tsc --noEmit with the same config in CI.