Practise TypeScript objects with 31 programs, each with a full solution, the exact output and a short explanation. The programs start with the basics, move to intermediate problems and finish with real-world scenarios from test automation, such as test data, prices, form inputs and test results. They are written for testers learning TypeScript for Playwright.
How to practise: read the task, write your own solution first, then open Show solution to compare. Run any program with npx tsx program.ts (or compile it with npx tsc).
New to this topic? Read the TypeScript Objects lesson first.
- Beginner programs (1–10)
- Intermediate programs (11–22)
- Real-world QA scenario programs (23–31)
TypeScript Beginner Programs
1. Inline object typeBeginner
Annotate a user object with an inline type and print its fields.
Show solution
const user: { name: string; age: number } = { name: 'Ana', age: 29 };
console.log(`${user.name} is ${user.age}`);
Output
Ana is 29
Explanation: the inline type lists each property and its type, so the object must match it exactly.
2. Missing property is a compile errorBeginner
Show that leaving out a required property is rejected.
Show solution
type Credentials = { username: string; password: string };
const ok: Credentials = { username: 'qa', password: 'secret' };
console.log(ok.username);
// @ts-expect-error: Property 'password' is missing in type '{ username: string; }'
const broken: Credentials = { username: 'qa' };
console.log(broken.username);
Output
qa
qa
Explanation: every property in the type is required unless marked optional, so a missing field fails to compile.
3. Type alias for an objectBeginner
Name an object shape with type and reuse it for two values.
Show solution
type Product = { sku: string; price: number };
const pen: Product = { sku: 'PEN-1', price: 1.5 };
const book: Product = { sku: 'BK-7', price: 12 };
console.log(`${pen.sku} + ${book.sku} = ${pen.price + book.price}`);
Output
PEN-1 + BK-7 = 13.5
Explanation: a type alias gives the shape a name, so you write it once and use it everywhere.
4. Optional propertyBeginner
Make phone optional and print a fallback when it is missing.
Show solution
type Contact = { email: string; phone?: string };
const contacts: Contact[] = [{ email: 'a@test.io', phone: '555-0101' }, { email: 'b@test.io' }];
for (const c of contacts) console.log(`${c.email}: ${c.phone ?? 'no phone'}`);
Output
a@test.io: 555-0101
b@test.io: no phone
Explanation: phone?: string means the property may be absent, so its type is string | undefined and needs a fallback.
5. Readonly propertyBeginner
Mark an id as readonly and show that it cannot be reassigned.
Show solution
type Order = { readonly id: number; status: string };
const order: Order = { id: 7, status: 'new' };
order.status = 'shipped';
// @ts-expect-error: Cannot assign to 'id' because it is a read-only property
const change = () => (order.id = 8);
console.log(order.id, order.status, typeof change);
Output
7 shipped function
Explanation: readonly blocks reassignment at compile time while other properties stay editable.
6. Excess property checkBeginner
Show that an object literal with an unknown property is rejected.
Show solution
type Point = { x: number; y: number };
const p: Point = { x: 1, y: 2 };
// @ts-expect-error: Object literal may only specify known properties, and 'z' does not exist in type 'Point'
const q: Point = { x: 1, y: 2, z: 3 };
console.log(p.x + p.y, q.x);
Output
3 1
Explanation: fresh object literals are checked for extra properties, which catches typos such as tiemout instead of timeout.
7. Nested object typeBeginner
Type a test config that contains a nested viewport object.
Show solution
type Config = { baseURL: string; viewport: { width: number; height: number } };
const config: Config = { baseURL: 'https://example.com', viewport: { width: 1280, height: 720 } };
console.log(`${config.baseURL} at ${config.viewport.width}x${config.viewport.height}`);
Output
https://example.com at 1280x720
Explanation: nested properties are typed too, so config.viewport.width is known to be a number.
8. Destructure with typesBeginner
Destructure a typed object and rename one property.
Show solution
type Env = { name: string; url: string; retries: number };
const env: Env = { name: 'staging', url: 'https://staging.example.com', retries: 2 };
const { name: envName, retries } = env;
console.log(`${envName} uses ${retries} retries`);
Output
staging uses 2 retries
Explanation: destructured variables take their types from the object, and name: envName renames while keeping the type.
9. Object method with typed parametersBeginner
Add a typed method to an object and call it.
Show solution
const calculator = {
rate: 0.2,
tax(amount: number): number {
return Math.round(amount * this.rate * 100) / 100;
},
};
console.log(calculator.tax(49.99));
Output
10
Explanation: methods in object literals can type their parameters and return value, and this refers to the object.
10. Object property access by a known keyBeginner
Read a property with bracket notation using a key typed as keyof.
Show solution
type Limits = { timeout: number; retries: number; workers: number };
const limits: Limits = { timeout: 30000, retries: 2, workers: 4 };
const key: keyof Limits = 'workers';
console.log(`${key} = ${limits[key]}`);
Output
workers = 4
Explanation: keyof Limits is 'timeout' | 'retries' | 'workers', so the lookup is safe and returns a number.
TypeScript Intermediate Programs
11. Record for a dictionaryIntermediate
Store status messages in Record<number, string> and look them up.
Show solution
const messages: Record<number, string> = { 200: 'OK', 404: 'Not Found', 500: 'Server Error' };
for (const code of [200, 404, 418]) console.log(`${code}: ${messages[code] ?? 'Unknown'}`);
Output
200: OK
404: Not Found
418: Unknown
Explanation: Record<K, V> describes an object used as a lookup table. A missing key still returns undefined at runtime, hence the fallback.
12. Record with a union of keysIntermediate
Require one timeout for every browser by keying a Record on a union.
Show solution
type Browser = 'chromium' | 'firefox' | 'webkit';
const timeouts: Record<Browser, number> = { chromium: 30000, firefox: 45000, webkit: 40000 };
console.log(Object.entries(timeouts).map(([b, t]) => `${b}=${t / 1000}s`).join(', '));
// @ts-expect-error: Property 'webkit' is missing in type '{ chromium: number; firefox: number; }'
const partial: Record<Browser, number> = { chromium: 1, firefox: 2 };
console.log(Object.keys(partial).length);
Output
chromium=30s, firefox=45s, webkit=40s
2
Explanation: when the keys are a union, every member must be present, so adding a browser later forces you to update the map.
13. Index signatureIntermediate
Allow any string key for custom HTTP headers.
Show solution
type Headers = { [name: string]: string };
const headers: Headers = { Accept: 'application/json' };
headers['X-Trace-Id'] = 'abc123';
for (const [k, v] of Object.entries(headers)) console.log(`${k}: ${v}`);
Output
Accept: application/json
X-Trace-Id: abc123
Explanation: an index signature says “any key of this type, with values of that type”, useful when keys are not known in advance.
14. Spread to copy and overrideIntermediate
Create a new config from defaults and overrides with spread.
Show solution
type RunConfig = { browser: string; headless: boolean; retries: number };
const defaults: RunConfig = { browser: 'chromium', headless: true, retries: 0 };
const ci: RunConfig = { ...defaults, retries: 2 };
console.log(defaults.retries, ci.retries, ci.browser);
Output
0 2 chromium
Explanation: spread copies properties into a new object, later properties win, and the result is still checked against RunConfig.
15. Partial for updatesIntermediate
Write update(user, changes) where changes may hold any subset of fields.
Show solution
type User = { name: string; email: string; active: boolean };
function update(user: User, changes: Partial<User>): User {
return { ...user, ...changes };
}
const before: User = { name: 'Ben', email: 'ben@test.io', active: true };
const after = update(before, { active: false });
console.log(after.name, after.active, before.active);
Output
Ben false true
Explanation: Partial<User> makes every property optional, which fits patch-style updates.
16. Required removes optionalIntermediate
Turn an options type with optional fields into a fully filled one.
Show solution
type Options = { timeout?: number; retries?: number };
function withDefaults(o: Options): Required<Options> {
return { timeout: o.timeout ?? 30000, retries: o.retries ?? 0 };
}
const full = withDefaults({ retries: 3 });
console.log(`timeout ${full.timeout / 1000}s, retries ${full.retries}`);
Output
timeout 30s, retries 3
Explanation: Required<Options> guarantees both numbers exist, so callers can use them without checking for undefined.
17. Pick and OmitIntermediate
Build a public profile type that leaves out the password.
Show solution
type Account = { id: number; email: string; password: string; role: string };
type PublicAccount = Omit<Account, 'password'>;
type Login = Pick<Account, 'email' | 'password'>;
const pub: PublicAccount = { id: 1, email: 'qa@test.io', role: 'admin' };
const login: Login = { email: 'qa@test.io', password: 'x' };
console.log(Object.keys(pub).join(','), Object.keys(login).join(','));
Output
id,email,role email,password
Explanation: Omit and Pick derive new types from an existing one, so they stay in sync when Account changes.
18. Readonly utility typeIntermediate
Freeze a settings object type with Readonly<T>.
Show solution
type Settings = { theme: string; lang: string };
const settings: Readonly<Settings> = { theme: 'dark', lang: 'en' };
// @ts-expect-error: Cannot assign to 'theme' because it is a read-only property
const change = () => (settings.theme = 'light');
console.log(settings.theme, typeof change);
Output
dark function
Explanation: Readonly<T> marks every property readonly in one step. Use Object.freeze too if you need runtime protection.
19. keyof with a generic getterIntermediate
Write getProp(obj, key) that only accepts keys that exist.
Show solution
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const test = { title: 'login', retries: 2, tags: ['smoke'] };
console.log(getProp(test, 'title').toUpperCase());
console.log(getProp(test, 'retries') + 1);
Output
LOGIN
3
Explanation: K extends keyof T limits the key, and T[K] gives the exact property type, so title is a string and retries a number.
20. Object.entries with typed valuesIntermediate
Print a score table and compute the total.
Show solution
const scores: Record<string, number> = { ana: 92, ben: 78, cai: 85 };
let total = 0;
for (const [student, score] of Object.entries(scores)) {
total += score;
console.log(`${student}: ${score}`);
}
console.log(`average ${(total / Object.keys(scores).length).toFixed(1)}`);
Output
ana: 92
ben: 78
cai: 85
average 85.0
Explanation: with a Record value type, Object.entries returns [string, number][], so score is a number.
21. Optional chaining on nested objectsIntermediate
Read a deeply nested optional value safely.
Show solution
type Response = { data?: { user?: { name: string } } };
const full: Response = { data: { user: { name: 'Ana' } } };
const empty: Response = {};
console.log(full.data?.user?.name ?? 'anonymous');
console.log(empty.data?.user?.name ?? 'anonymous');
Output
Ana
anonymous
Explanation: ?. stops at the first undefined and returns undefined, and the type of the chain includes undefined to match.
22. Object comparison by valueIntermediate
Compare two flat objects field by field.
Show solution
function shallowEqual(a: Record<string, unknown>, b: Record<string, unknown>): boolean {
const ka = Object.keys(a);
const kb = Object.keys(b);
return ka.length === kb.length && ka.every(k => a[k] === b[k]);
}
const a = { id: 1, status: 'ok' };
const copy = { ...a };
console.log(a === copy, shallowEqual(a, copy), shallowEqual(a, { id: 1, status: 'err' }));
Output
false true false
Explanation: === compares object references, not contents. unknown values can still be compared with === without any casts.
TypeScript Real-World QA Scenario Programs
23. Deep merge of configReal-world
Merge nested overrides into a default config without changing the defaults.
Show solution
type Cfg = { use: { headless: boolean; viewport: { width: number; height: number } }; retries: number };
type DeepPartial<T> = { [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K] };
const base: Cfg = { use: { headless: true, viewport: { width: 1280, height: 720 } }, retries: 0 };
function merge(cfg: Cfg, o: DeepPartial<Cfg>): Cfg {
return {
retries: o.retries ?? cfg.retries,
use: {
headless: o.use?.headless ?? cfg.use.headless,
viewport: { ...cfg.use.viewport, ...o.use?.viewport },
},
};
}
const mobile = merge(base, { use: { viewport: { width: 390 } } });
console.log(mobile.use.viewport.width, mobile.use.viewport.height, base.use.viewport.width);
Output
390 720 1280
Explanation: a recursive mapped type makes every nested property optional, so callers can override just one deep field.
24. Mapped type for flagsReal-world
Create a type with a boolean flag for each feature name.
Show solution
type Feature = 'darkMode' | 'beta' | 'chat';
type Flags = { [F in Feature]: boolean };
const flags: Flags = { darkMode: true, beta: false, chat: true };
const enabled = (Object.keys(flags) as Feature[]).filter(f => flags[f]);
console.log(`enabled: ${enabled.join(', ')}`);
Output
enabled: darkMode, chat
Explanation: a mapped type builds properties from a union, so the object must have exactly one flag per feature.
25. Type-safe object keys helperReal-world
Write a keys helper that returns (keyof T)[] instead of string[].
Show solution
function keys<T extends object>(obj: T): (keyof T)[] {
return Object.keys(obj) as (keyof T)[];
}
const report = { passed: 18, failed: 2, skipped: 1 };
for (const k of keys(report)) console.log(`${k.padEnd(8)}${report[k]}`);
Output
passed 18
failed 2
skipped 1
Explanation: Object.keys returns string[] because objects may have extra keys at runtime. When you know the shape, a typed helper lets you index without errors.
26. Discriminated union of objectsReal-world
Handle API results that are either success or error objects.
Show solution
type ApiResult =
| { ok: true; data: { id: number } }
| { ok: false; error: string };
function describe(r: ApiResult): string {
return r.ok ? `created #${r.data.id}` : `failed: ${r.error}`;
}
console.log(describe({ ok: true, data: { id: 42 } }));
console.log(describe({ ok: false, error: '401 Unauthorized' }));
Output
created #42
failed: 401 Unauthorized
Explanation: checking the shared ok property narrows the union, so data is only reachable on success and error only on failure.
27. satisfies keeps literal typesReal-world
Check a route table against a type while keeping its exact keys.
Show solution
type Route = { path: string; auth: boolean };
const routes = {
home: { path: '/', auth: false },
account: { path: '/account', auth: true },
} satisfies Record<string, Route>;
console.log(routes.account.path, Object.keys(routes).length);
// @ts-expect-error: Property 'admin' does not exist on type
const missing = () => routes.admin;
console.log(typeof missing);
Output
/account 2
function
Explanation: satisfies checks the shape but keeps the inferred type, so unknown keys like admin are still caught, unlike a plain Record<string, Route> annotation.
28. Build an object from entriesReal-world
Convert an array of [name, value] cookie pairs into a typed object.
Show solution
const pairs: [string, string][] = [['session', 'abc'], ['theme', 'dark'], ['lang', 'en']];
const cookies: Record<string, string> = Object.fromEntries(pairs);
console.log(cookies.theme, Object.keys(cookies).length);
Output
dark 3
Explanation: Object.fromEntries turns pairs back into an object, the reverse of Object.entries.
29. Freeze test data at runtime and compile timeReal-world
Combine as const and Object.freeze for fixed test users.
Show solution
const USERS = Object.freeze({
admin: { email: 'admin@test.io', role: 'admin' },
viewer: { email: 'viewer@test.io', role: 'viewer' },
} as const);
type Role = (typeof USERS)[keyof typeof USERS]['role'];
const role: Role = USERS.viewer.role;
console.log(role, Object.isFrozen(USERS));
Output
viewer true
Explanation: as const gives readonly literal types for the compiler, Object.freeze stops changes at runtime, and the role union is derived from the data.
30. Pick properties at runtime with typesReal-world
Write a generic pick(obj, keys) that returns only the chosen fields.
Show solution
function pick<T extends object, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
const out = {} as Pick<T, K>;
for (const k of keys) out[k] = obj[k];
return out;
}
const fullUser = { id: 9, email: 'cai@test.io', password: 'hidden', role: 'qa' };
const safe = pick(fullUser, ['id', 'email', 'role']);
console.log(JSON.stringify(safe));
Output
{"id":9,"email":"cai@test.io","role":"qa"}
Explanation: the return type Pick<T, K> matches exactly the keys passed in, so safe.password would be a compile error.
31. Count fields by value typeReal-world
Summarise an object’s values by their runtime type.
Show solution
function typeSummary(obj: Record<string, unknown>): Record<string, number> {
const out: Record<string, number> = {};
for (const v of Object.values(obj)) {
const t = Array.isArray(v) ? 'array' : v === null ? 'null' : typeof v;
out[t] = (out[t] ?? 0) + 1;
}
return out;
}
const payload = { id: 1, name: 'x', tags: ['a'], active: true, note: null, score: 9.5 };
console.log(JSON.stringify(typeSummary(payload)));
Output
{"number":2,"string":1,"array":1,"boolean":1,"null":1}
Explanation: values typed as unknown must be checked before use, which is the safe way to handle data from an API.
2 thoughts on “TypeScript Objects Practice Programs with Solutions (31 Programs)”