TypeScript Objects Practice Programs with Solutions (31 Programs)

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.

Advertisement

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.

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&lt;K, V&gt; 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&lt;User&gt; 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&lt;Options&gt; 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&lt;T&gt;.

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&lt;T&gt; 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&lt;string, Route&gt; 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&lt;T, K&gt; 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.

Keep Practising

2 thoughts on “TypeScript Objects Practice Programs with Solutions (31 Programs)”

Leave a Comment