This content originally appeared on DEV Community and was authored by MUHAMMAD USMAN AWAN
Level up your TypeScript skills by learning interfaces, classes, generics, utility types, and more — the building blocks for real-world projects.
Hey everyone 👋, I’m back again with Part 2 of my TypeScript learning series! If you missed the first part, we covered all the beginner foundations of TypeScript — from setup and basic types to unions and enums. Now, it’s time to take a step forward into the intermediate concepts that make TypeScript powerful in real-world applications. In this part, we’ll explore interfaces vs types, classes and OOP, generics, type narrowing, modules, utility types, DOM typings, and error handling. Buckle up — this is where TS really starts to shine for actual projects! 🚀
📗 Intermediate TypeScript (Core Power)
🔹 1. Interfaces vs Type Aliases Interface
interface User {
id: number;
name: string;
}
const user1: User = { id: 1, name: "Usman" };
Type Alias
type UserType = {
id: number;
name: string;
};
const user2: UserType = { id: 2, name: "Ali" };
✅ Key difference:
- Interfaces: can be extended & merged.
- Types: more flexible (unions, intersections), but can’t merge.
Extending Interfaces
interface Person {
name: string;
}
interface Employee extends Person {
salary: number;
}
const emp: Employee = { name: "Ali", salary: 50000 };
Declaration Merging (only interfaces!)
interface Car {
brand: string;
}
interface Car {
year: number;
}
const myCar: Car = { brand: "Toyota", year: 2023 };
🔹 2. Classes & OOP in TS
class Animal {
public name: string; // accessible everywhere
protected age: number; // accessible in class + subclass
private secret: string; // only inside this class
static count: number = 0; // belongs to class, not instance
readonly type: string; // cannot be reassigned
constructor(name: string, age: number, type: string) {
this.name = name;
this.age = age;
this.secret = "hidden";
this.type = type;
Animal.count++;
}
public speak(): void {
console.log(`${this.name} makes a sound`);
}
}
class Dog extends Animal {
constructor(name: string, age: number) {
super(name, age, "dog");
}
public bark() {
console.log(`${this.name} barks!`);
}
}
const d = new Dog("Buddy", 3);
d.speak(); // Buddy makes a sound
d.bark(); // Buddy barks!
Abstract Classes
abstract class Shape {
abstract area(): number; // must be implemented in subclass
}
class Circle extends Shape {
constructor(public radius: number) { super(); }
area(): number {
return Math.PI * this.radius ** 2;
}
}
const c = new Circle(5);
console.log(c.area());
🔹 3. Generics
Generic Function
function identity<T>(arg: T): T {
return arg;
}
console.log(identity<string>("Hello")); // Hello
console.log(identity<number>(123)); // 123
Generic Interfaces
interface Box<T> {
value: T;
}
let stringBox: Box<string> = { value: "text" };
let numberBox: Box<number> = { value: 100 };
Generic Constraints
function logLength<T extends { length: number }>(item: T) {
console.log(item.length);
}
logLength("hello"); // ✅
logLength([1,2,3]); // ✅
Default Generic Types
interface ApiResponse<T = string> {
data: T;
}
const res1: ApiResponse = { data: "ok" }; // string by default
const res2: ApiResponse<number> = { data: 42 };
🔹 4. Type Narrowing
function printId(id: string | number) {
if (typeof id === "string") {
console.log("String ID:", id.toUpperCase());
} else {
console.log("Number ID:", id.toFixed(2));
}
}
Instanceof
class Cat { meow() {} }
class Dog { bark() {} }
function sound(pet: Cat | Dog) {
if (pet instanceof Cat) pet.meow();
else pet.bark();
}
Discriminated Union
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number };
function area(shape: Shape) {
switch (shape.kind) {
case "circle": return Math.PI * shape.radius ** 2;
case "square": return shape.side * shape.side;
}
}
🔹 5. Modules & Namespaces
ES Modules
// file: math.ts
export function add(a: number, b: number) {
return a + b;
}
export default function multiply(a: number, b: number) {
return a * b;
}
// file: app.ts
import multiply, { add } from "./math";
console.log(add(2, 3));
console.log(multiply(2, 3));
Namespaces (rarely used now, mostly in legacy code)
namespace Utils {
export function greet(name: string) {
return `Hello, ${name}`;
}
}
console.log(Utils.greet("Usman"));
🔹 6. Type Assertions & Casting
let value: unknown = "hello";
let strLength: number = (value as string).length;
let el = document.querySelector("#myInput") as HTMLInputElement;
el.value = "typed!";
Non-null Assertion (!)
let btn = document.querySelector("#btn")!;
btn.addEventListener("click", () => console.log("Clicked!"));
🔹 7. Utility Types (Built-in)
interface User {
id: number;
name: string;
age?: number;
}
// Make all properties optional
type PartialUser = Partial<User>;
// Make all properties required
type RequiredUser = Required<User>;
// Make all properties readonly
type ReadonlyUser = Readonly<User>;
// Pick selected properties
type UserName = Pick<User, "name">;
// Omit selected properties
type UserWithoutAge = Omit<User, "age">;
// Map keys to type
type Roles = Record<"admin" | "user", boolean>;
// Extract function return/params
function getUser() { return { id: 1, name: "Ali" }; }
type UserReturn = ReturnType<typeof getUser>;
🔹 8. Working with DOM & Browser APIs
const input = document.querySelector<HTMLInputElement>("#username");
if (input) {
input.value = "Usman Awan";
}
document.addEventListener("click", (e: MouseEvent) => {
console.log(e.clientX, e.clientY);
});
document.addEventListener("keydown", (e: KeyboardEvent) => {
console.log(e.key);
});
🔹 9. Error Handling Types
Typing try/catch
try {
throw new Error("Something went wrong");
} catch (err: unknown) {
if (err instanceof Error) {
console.error("Error:", err.message);
}
}
unknown
vs any
-
any
: bypasses type checking (unsafe). -
unknown
: forces you to narrow type before using.
let val: unknown = "test";
val.toUpperCase(); // ❌ error
if (typeof val === "string") {
console.log(val.toUpperCase()); // ✅ safe
}
✅ Intermediate Summary
You now know how to:
- Use interfaces vs types, extend & merge.
- Write classes, abstract classes, OOP with access modifiers.
- Work with generics for reusable code.
- Narrow types with typeof, instanceof, discriminated unions.
- Import/export using modules.
- Safely cast values & use type assertions.
- Apply utility types like Partial, Pick, Omit.
- Handle DOM events with strict types.
- Properly type error handling.
That’s a wrap for Part 2 (Intermediate) of our TypeScript journey! 🎉 You now know how to confidently use interfaces, classes, generics, utility types, and type-safe DOM handling. With these tools, you’re ready to start applying TypeScript in real projects — whether it’s a Node.js API, a React app, or even a library. In the next and final part (Advanced), we’ll dive into conditional types, mapped types, advanced generics, declaration files, and the latest TS features. Stay tuned, it’s going to be next-level! 💡
Thanks for reading! 🙌
Until next time, 🫡
Usman Awan (your friendly dev 🚀)
This content originally appeared on DEV Community and was authored by MUHAMMAD USMAN AWAN

MUHAMMAD USMAN AWAN | Sciencx (2025-08-25T08:59:54+00:00) Mastering TypeScript from Scratch – Part 2: Intermediate Concepts. Retrieved from https://www.scien.cx/2025/08/25/mastering-typescript-from-scratch-part-2-intermediate-concepts-3/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.