Practice Questions
1. Show the syntax for declaring variables with type annotations for: a string, a number, a boolean, and an array of numbers. Then show the same declarations without annotations and explain why they still work.
Solution
With explicit annotations:
const name: string = "Ali";
const age: number = 30;
const active: boolean = true;
const scores: number[] = [90, 85, 100];
Without annotations:
const name = "Ali";
const age = 30;
const active = true;
const scores = [90, 85, 100];
TypeScript infers the types from the assigned values. When you initialize a variable with a value, TypeScript automatically determines the type, so explicit annotations are unnecessary in these cases.
2. Write a TypeScript function called formatId that accepts a parameter of type string | number. If the value is a string, return it uppercased. If it is a number, return it as a string prefixed with "#".
Solution
function formatId(id: string | number): string {
if (typeof id === "string") {
return id.toUpperCase();
}
return `#${id}`;
}
formatId("abc"); // "ABC"
formatId(42); // "#42"
The typeof check is a type guard that narrows the union type. Inside the if block, TypeScript knows id is a string; in the else branch, it knows id is a number.
3. What is the difference between type and interface in TypeScript? List at least three differences and explain when you would use each.
Solution
| Feature | interface |
type |
|---|---|---|
| Object shapes | Yes | Yes |
| Extends / inheritance | extends keyword |
Intersection (&) |
| Declaration merging | Yes (can reopen and add properties) | No |
| Unions / intersections | No (not directly) | Yes |
| Primitives, tuples, etc. | No | Yes |
Use interface when defining the shape of objects and classes (especially if you want declaration merging or implements). Use type when you need unions, intersections, tuples, or when aliasing primitives. When describing a plain object shape where either would work, both are fine.
4. Define an interface called Product with a name (string), price (number), and optional description (string). Then use utility types to create: (a) a type where all properties are optional, and (b) a type with only name and price.
Solution
interface Product {
name: string;
price: number;
description?: string;
}
// (a) All properties optional
type PartialProduct = Partial<Product>;
// { name?: string; price?: number; description?: string }
// (b) Only name and price
type ProductSummary = Pick<Product, "name" | "price">;
// { name: string; price: number }
Partial<T> makes every property optional. Pick<T, K> selects specific properties. You could also use Omit<Product, "description"> for (b).
5. Write a generic function called firstElement that takes an array of any type T and returns the first element or undefined if the array is empty. Show how it works with both explicit and inferred type parameters.
Solution
function firstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
// Explicit type parameter
const a = firstElement<string>(["hello", "world"]); // "hello"
// Inferred type parameter
const b = firstElement([1, 2, 3]); // 1 — TypeScript infers T as number
const c = firstElement([]); // undefined
The generic <T> acts as a placeholder. When you call the function, TypeScript either uses the type you explicitly provide or infers it from the argument.
6. What are the three access modifiers TypeScript adds to class members? For each modifier, describe where the member is accessible. Then write a short class that uses all three.
Solution
| Modifier | Accessible From |
|---|---|
public |
Anywhere (this is the default) |
private |
Only within the class itself |
protected |
Within the class and its subclasses |
class Employee {
public name: string;
protected department: string;
private salary: number;
constructor(name: string, department: string, salary: number) {
this.name = name;
this.department = department;
this.salary = salary;
}
getSalary(): number {
return this.salary;
}
}
const emp = new Employee("Ali", "Engineering", 90000);
console.log(emp.name); // OK — public
// emp.department; // Error — protected
// emp.salary; // Error — private
console.log(emp.getSalary()); // OK — public method accessing private field
7. Why can you not use instanceof to check if a value matches an interface at runtime? What should you do instead?
Solution
Interfaces (and type aliases) are erased during compilation — they produce no JavaScript output. At runtime, there is no User object to check against, so value instanceof User causes a compile error.
interface User {
name: string;
age: number;
}
// This does NOT work:
// value instanceof User
// Error: 'User' only refers to a type, but is being used as a value here
Instead, write runtime checks using typeof, in, or custom type predicate functions:
function isUser(value: unknown): value is User {
if (typeof value !== "object" || value === null) {
return false;
}
const candidate = value as { name?: unknown; age?: unknown };
return (
typeof candidate.name === "string" && typeof candidate.age === "number"
);
}
The value is User return type is a type predicate that tells TypeScript the value can be safely treated as User when the function returns true.
8. A colleague has written the following function. Explain what is wrong with this approach and rewrite it using unknown with proper type narrowing.
function processData(data: any) {
return data.name.toUpperCase();
}
Solution
The any type disables all type checking. If data is not an object with a name property, or if name is not a string, this will crash at runtime with no warning from the compiler.
Rewritten with unknown:
function processData(data: unknown): string {
if (
typeof data === "object" &&
data !== null &&
"name" in data &&
typeof (data as { name: unknown }).name === "string"
) {
return (data as { name: string }).name.toUpperCase();
}
throw new Error("Invalid data: expected an object with a string 'name'");
}
Using unknown forces you to check the shape of the data before you access any property. The compiler will not let the unchecked access through, so the function reports a bad input instead of failing at runtime.
9. What does as const do in TypeScript? Show an example with an object and an array, and explain how the inferred types differ with and without as const.
Solution
The as const assertion tells TypeScript to infer the narrowest (most literal) type possible and makes all properties readonly.
// Without as const
const config = { api: "https://api.example.com", retries: 3 };
// Type: { api: string; retries: number }
// With as const
const configNarrow = { api: "https://api.example.com", retries: 3 } as const;
// Type: { readonly api: "https://api.example.com"; readonly retries: 3 }
// Without as const
const point = [10, 20];
// Type: number[]
// With as const
const pointNarrow = [10, 20] as const;
// Type: readonly [10, 20]
Without as const, TypeScript widens the types (string, number, number[]). With as const, it preserves the exact literal values and treats arrays as readonly tuples. This is useful when you need TypeScript to know the exact values, such as when passing literal strings to functions that expect specific string unions.
10. Given the following tsconfig.json options, explain what each one does:
{
"compilerOptions": {
"target": "ES2020",
"module": "ES2020",
"strict": true,
"outDir": "./dist",
"esModuleInterop": true
},
"include": ["src/**/*.ts"]
}
Solution
target: The JavaScript version to compile to.ES2020means the output will use ES2020 features.module: The module system for the output code (e.g., ES2020 modules withimport/export, or CommonJS withrequire).strict: Enables all strict type-checking options, includingstrictNullChecks(prevents usingnull/undefinedwhere a value is expected) and other safety checks. Always recommended for new projects.outDir: The directory where compiled.jsfiles are placed (./distin this case).esModuleInterop: Allows default imports from CommonJS modules (e.g.,import express from "express"instead ofimport * as express).include: Specifies which files to compile — here, all.tsfiles under thesrcdirectory.