Practice Learn TypeScript curriculum
Curriculum
Draft
Private preview - this page is unpublished.
Sidebar link will be added once content is approved. Not indexed by search engines.
Curriculum . TypeScript

Learn TypeScript for Playwright - 5 chapters, 6 modules

TypeScript is what every serious Playwright codebase ships in. Once your JavaScript is solid, adding a type system gives you autocomplete on every locator, refactor safety on every page object, and a compiler that catches half your bugs before the browser opens. This page covers chapters 18 to 22 of our internal TS track and groups the topics into six modules - from tsconfig.json to generics and abstract classes. Every module points at a TTA project (the V1 framework, the TTACart suite) where you actually apply the patterns. We summarise the curriculum here, not the source - write the answers yourself.

5
Chapters covered
Chapters 18-22 of the internal track, six teaching modules.
38
Sub-topics
From strict mode to readonly parameter properties.
60
Exercises
Approx count of write-the-type, infer-the-shape, narrow-this-union drills.

01TS setup + basics

Topics 175-178. Get the compiler running, write the first hello-world, and learn the seven type annotations you will see in 80 percent of Playwright code.

Sub-topics covered

tsc install + init tsconfig.json strict mode target + module esModuleInterop Hello world compile number / string / boolean arrays + tuples type vs interface
flowchart LR
  TS[app.ts] --> P[Parser]
  P --> A[AST]
  A --> CHK[Type checker]
  CHK -->|ok| EMIT[Emit phase]
  CHK -->|fail| ERR[compile error]
  EMIT --> JS[app.js + .d.ts]
  JS --> N[Node / Browser runs]
tsc compile pipeline - typecheck blocks emit on error
strict: true bundles six flags noImplicitAny strictNullChecks strictFunctionTypes strictBindCallApply strictPropertyInit alwaysStrict turn on day one - paying the bill later is much harder
strict mode - six sub-flags it enables

Exercises typical for this module

  • Initialise a fresh tsconfig.json with strict: true and explain in two lines what each of the six strict* flags catches.
  • Convert a JS file that uses implicit any into a strict-mode TS file. Add the minimum annotations needed to make it compile.
  • Write a function signature for greet(name: string, age?: number): string and call it three ways - with and without the optional.
  • Compare const arr: number[] vs const arr: Array<number> vs a tuple [number, string]. When does each matter.
number[] [1, 2, 3] any length all numbers homogeneous prices, ids Array<number> same as [] generic form readable in unions style choice utility chains [number, string] FIXED length 2 pos 0 = number pos 1 = string tuple react useState
Exercise answer key: array vs Array<T> vs tuple
greet(name: string, age?: number): string greet('Aman') age undefined greet('Aman', 32) age = 32 greet() // compile error name is required
Exercise answer key: optional parameter call shapes
Apply at: /playwright/advance-framework.html tsconfig + folders

02Types deep dive

Topics 179-183. Once string and number click, the next level is unknown, narrowing, and the typed array methods that come for free.

Sub-topics covered

any vs unknown never + void union + intersection literal types type narrowing typeof / instanceof guards in operator narrowing discriminated unions array.filter type predicates
flowchart TD
  X[x: string | number] --> G{typeof x === 'string'?}
  G -- yes --> S[narrowed to string -> .toUpperCase ok]
  G -- no  --> N[narrowed to number -> .toFixed ok]
Type narrowing - control flow narrows the type
any let x: any x.foo.bar() // OK x() // OK x + 'z' // OK no checks - bypass avoid in new code unknown let x: unknown x.foo // ERROR must narrow first if (typeof x...) ok type-safe top type prefer for foreign data
any vs unknown - both top types, only one is safe
flowchart LR
  P1[Cat: kind, meow] --> S[structural check]
  P2[CatLike: kind, meow] --> S
  S --> EQ[TS treats them as the SAME type]
Structural typing - shape matters, name does not

Exercises typical for this module

  • Migrate a callback API that uses any for the response into one that uses unknown, then narrow it with typeof and in before using it.
  • Write a filterNonNull<T> that turns Array<T | null> into Array<T> using a type predicate (x): x is T => x !== null.
  • Build a discriminated union for a test result: { status: 'pass'; ms: number } | { status: 'fail'; ms: number; error: string } and write a function that handles both branches.
  • Explain when a function should return void vs never. Give one example each from a Playwright test helper.
  • Use a literal type to restrict sortBy to 'az' | 'za' | 'lohi' | 'hilo' and watch the compiler reject typos.
discriminated union by status { status: 'pass', ms } | { status: 'fail', ms, error } if status === 'pass' r.ms (number) no error field if status === 'fail' r.error (string) narrowed by literal
Exercise answer key: discriminated union for test result
void function log(): void does NOT return a useful value control returns to caller e.g. console.log, await page.click(...) never function fail(): never control NEVER returns throws OR loops forever used in exhaustiveness e.g. throw new Error, default: assertNever(x)
Exercise answer key: void (returns) vs never (does not)
type SortBy = 'az' | 'za' | 'lohi' | 'hilo' sortBy('az') // OK matches literal sortBy('AZ') // ERROR typo caught by tsc literal unions = enum-lite, autocomplete + typo safety no runtime cost - erased at compile
Exercise answer key: literal union for sortBy

03Interfaces - the contract

Chapter 19. Interfaces describe the shape of an object. They are the way a TTACart product, a login payload, or a checkout step talks to the rest of the framework.

Sub-topics covered

interface declaration optional properties (?) readonly fields method signatures extending interfaces implements on a class index signatures interface vs type alias declaration merging
classDiagram
  class UserBase {
    +id: string
    +email: string
  }
  class AdminUser {
    +permissions: string[]
  }
  UserBase <|-- AdminUser
Interface extends - AdminUser inherits all UserBase fields
interface modifiers - optional + readonly interface Product { readonly sku : string; // can't reassign name : string; // required priceInPaise : number; description? : string; // may be undefined }
Optional (?) and readonly modifiers in an interface

Exercises typical for this module

  • Convert a JS object literal that represents a TTACart product into an interface Product. Mark the description optional and the SKU readonly.
  • Extend interface UserBase with interface AdminUser that adds a permissions: string[] field. Show that a function accepting UserBase still accepts an AdminUser.
  • Write a class CheckoutStepOnePage implements ICheckoutPage and let the compiler tell you which methods are missing.
  • Use an index signature to type a dynamic env map: interface Env { [key: string]: string | undefined } and explain why undefined matters.
  • Pick: type vs interface for a union of 'desktop' | 'mobile'. Justify in two lines.
interface interface P { name: string } object shapes declaration merging extends keyword pick for: classes, public API, OOP implements on class type alias type D = 'desktop' | 'mobile' unions, tuples, conditional, mapped no merging pick for: unions, primitives, utility never extends classes
Exercise answer key: interface vs type alias
interface Env { [k: string]: string | undefined } env['NODE_ENV'] type: string | undefined env['MISSING'] type: string | undefined | undefined forces null-check on every read without it, env.X looks safe even when key is missing
Exercise answer key: index signature with | undefined

A small TTA-targeted interface - illustrative only, written fresh:

illustrative Product + CartLine
interface Product {
  readonly sku: string;
  name: string;
  priceInPaise: number;
  description?: string;
}

interface CartLine extends Product {
  qty: number;
}

const line: CartLine = {
  sku: 'tta-001', name: 'Mug', priceInPaise: 19900, qty: 2
};

04Enums - named constants

Chapter 20. Enums replace stringly-typed switches with named buckets. Useful for env names, test tags, sort orders, and checkout states.

Sub-topics covered

Numeric enums String enums Reverse mapping const enum Enum vs union literal Heterogeneous (avoid) Enum as discriminator Computed members
numeric enum - auto-increment from 0 enum Direction { Up, Down, Left, Right } Up = 0 default start Down = 1 +1 Left = 2 +1 Right = 3 +1 Direction[0] === 'Up' // reverse mapping numeric enums emit both name -> value AND value -> name
Numeric enum auto-increment + reverse mapping
string enum - explicit, no reverse map enum Env { Local='local', Stage='stage', Prod='prod' } Local 'local' Stage 'stage' Prod 'prod' Env['local'] // undefined - no reverse map string enums skip the reverse table to save bytes
String enum - explicit values, one-way mapping

Exercises typical for this module

  • Define a string enum Env with members Local, Stage, Prod. Use it as the parameter type for a getBaseUrl(env: Env) helper.
  • Show the reverse mapping that a numeric enum produces (Env[0] === 'Local'). Explain why string enums do not.
  • Replace a 5-branch switch with a lookup-object keyed by a string enum. Compare readability.
  • Decide: when do you pick const enum SortOrder over type SortOrder = 'az' | 'za'. Two sentences.
  • Use an enum as the discriminator field in a tagged union of test-result payloads.
const enum SortOrder const enum SO { Az, Za } erased at compile inlined as 0, 1 pros: zero runtime cons: --isolatedModules cons: no reverse type SortOrder union type SO = 'az' | 'za' erased at compile string literals stay pros: simpler, faster pros: no bundler trap prefer for small sets
Exercise answer key: const enum vs literal union
Apply at: /playwright/advance-framework.html env + tags

05Generics - reusable types

Chapter 21. The feature that makes a UtilElementLocator, a fixture, or a readJson helper work across types without losing inference.

Sub-topics covered

Generic functions Generic interfaces Generic classes Constraints (extends) Default type params keyof + indexed access Conditional types infer keyword Utility types (Partial / Pick / Record)
flowchart LR
  CALL1[id(42)] --> INF1[T inferred as number]
  CALL2[id('hi')] --> INF2[T inferred as string]
  CALL3[id<boolean>(true)] --> EXPL[T explicit boolean]
  INF1 --> RET1[returns number]
  INF2 --> RET2[returns string]
  EXPL --> RET3[returns boolean]
Single generic T - inference flows from argument to return
flowchart LR
  F[pair<T, U>(t, u)] --> T[T from first arg]
  F --> U[U from second arg]
  T --> R[[t, u]: [T, U]]
  U --> R
Multiple type params - T and U inferred independently
flowchart LR
  S[T extends { id: string }] --> A[ok: { id, name }]
  S --> B[ok: { id, email, age }]
  S --> C[FAIL: { name } - no id]
  S --> D[FAIL: number - no id]
Constraint <T extends X> - only types matching X allowed

Exercises typical for this module

  • Write a generic wrap<T>(value: T): { value: T } and test the inference - hover over the return type for three call sites.
  • Implement readJson<T>(path: string): Promise<T>, then call it with an explicit type argument readJson<User[]>('users.json').
  • Add a constraint: function getId<T extends { id: string }>(x: T) and prove the compiler now rejects objects without id.
  • Use Partial<Product> to type a patch-update helper. Add a Required<Product, 'sku'> variant.
  • Write a generic Page Object base: class BasePage<L extends Locators> where L describes the locator dictionary.

Generic Page Object - flow

flowchart LR
  A[BasePage<L>] --> B[LoginPage with L = LoginLocators]
  A --> C[InventoryPage with L = InvLocators]
  A --> D[CartPage with L = CartLocators]
  B --> E[expect on this.locators.user]
  C --> F[click on this.locators.sortSelect]
  D --> G[count on this.locators.cartItems]
function id<T>(x: T): T - inference id(42) T = number id('hi') T = string id<Date>(d) T = Date no annotation -> T inferred from arg explicit <T> helps when arg is ambiguous
Exercise answer key: generic identity inference table
Partial<Product> - all fields optional Product sku: string name: string price: number all required Partial<Product> sku?: string name?: string price?: number use for PATCH
Exercise answer key: Partial maps every field to optional
readJson<T>(path): Promise<T> readJson('u.json') T = unknown (default) readJson<User[]>('u.json') T = User[] - safe access explicit type arg shapes the return Promise runtime still parses JSON - add a zod check for real safety
Exercise answer key: explicit type arg for readJson

06Access modifiers + classes

Chapter 22. The final piece. public, private, protected, readonly, parameter properties, abstract classes - the syntax that ties Page Object Model + fixtures together.

Sub-topics covered

public (default) private vs #private protected readonly fields Parameter properties abstract class + method static members getters / setters implements + extends
class CartPage - member badges public page: Page // anyone protected getRow(i) // subclass only private apiKey // this class only (TS check) readonly url: 'cart.html' // set once static from(page) // on the class itself
Access modifiers - public, protected, private, readonly, static

Exercises typical for this module

  • Refactor a constructor with five manual this.x = x assignments into parameter properties. Compare line counts.
  • Make a BasePage abstract with one abstract method readonly url: string. Force every subclass to declare its own URL.
  • Pick: TS private vs JS # for the same field. Which one is compile-time only, which is run-time enforced.
  • Add a static factory: InventoryPage.from(page: Page): InventoryPage. Discuss why that beats a free function.
  • Combine readonly + parameter property to build an immutable TestUser class.
before - 7 lines class U { id: string name: string ctor(id, name) { this.id = id this.name = name } } after - 3 lines class U { ctor( public readonly id: string, public name: string, ) {} } visibility on the param auto-assigns to this
Exercise answer key: parameter properties save lines
TS private private apiKey compile-time only erased in JS output obj['apiKey'] works at runtime use for OOP discipline JS #private #apiKey runtime enforced in the JS output obj.#apiKey only inside the class use for secrets, tokens
Exercise answer key: private (compile) vs # (runtime)

A small TTA-targeted abstract page - illustrative only, written fresh:

illustrative abstract BasePage
abstract class BasePage {
  constructor(protected readonly page: Page) {}
  abstract readonly url: string;
  async open(): Promise<void> { await this.page.goto(this.url); }
}

class CartPage extends BasePage {
  readonly url = '/playwright/ttacart/cart.html';
  get items() { return this.page.locator('.cart-line'); }
}

Where you apply TypeScript

Two living TTA projects where the modules above stop being concepts and start being merged commits. Open them side-by-side with this page.