TypeScript is a powerful superset of JavaScript that introduces static typing. It allows us to write safer, more predictable code. But just defining types isn’t enough — in real-world applications, we often deal with runtime data (user input, API responses, dynamic objects, etc.) that can bypass the compiler’s safety net.
This blog post serves as your one-stop solution for all type checking techniques in TypeScript, covering basic to advanced concepts with organized sections and practical code examples.
🧱 1. TypeScript Basics: Compile-Time Type Checking
✅ Static Type Annotations
let age: number = 30;
let name: string = "Alice";
let isActive: boolean = true;
✅ Arrays and Tuples
let scores: number[] = [90, 85, 70];
let user: [string, number] = ["Bob", 25]; // Tuple
✅ Enums
enum Role {
Admin,
User,
Guest,
}
const userRole: Role = Role.User;
🧾 2. Using typeof
for Primitive Runtime Checks
TypeScript types vanish at runtime, so we use JS operators like typeof
to check types dynamically.
function logValue(value: unknown) {
if (typeof value === "string") {
console.log("It's a string:", value.toUpperCase());
} else if (typeof value === "number") {
console.log("It's a number:", value.toFixed(2));
} else {
console.log("Unknown type");
}
}
Supported typeof
types:
"string"
"number"
"boolean"
"undefined"
"object"
"function"
"symbol"
"bigint"
📏 3. Using instanceof
for Class Checks
Use instanceof
to check if an object is an instance of a class.
class Animal {
name: string = "Animal";
}
class Dog extends Animal {
breed: string = "Labrador";
}
const pet = new Dog();
if (pet instanceof Dog) {
console.log(pet.breed); // Labrador
}
🪪 4. Type Guards (Custom Runtime Type Checks)
✅ Type Predicate Function
type User = { name: string; age: number };
type Admin = { name: string; role: string };
function isAdmin(user: User | Admin): user is Admin {
return (user as Admin).role !== undefined;
}
const u: User | Admin = { name: "Alex", role: "admin" };
if (isAdmin(u)) {
console.log("Admin role is:", u.role);
}
🔥
user is Admin
is a type predicate that narrows the type inside conditionals.
🧱 5. Discriminated Unions
Very powerful way to model and type-check multiple variants.
type Circle = { kind: "circle"; radius: number };
type Square = { kind: "square"; size: number };
type Shape = Circle | Square;
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.size ** 2;
}
}
✅ Discriminated unions help safely branch logic based on a common
kind
property.
📚 6. in
Operator for Property Checks
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ("swim" in animal) {
animal.swim();
} else {
animal.fly();
}
}
✅ Use the
in
operator when there’s no common discriminator (kind
field).
🧮 7. Type Assertions (Be Careful!)
const someValue: unknown = "Hello World";
const strLength: number = (someValue as string).length;
⚠️ Type assertions override the compiler. Use sparingly when you’re absolutely sure.
🧊 8. Using typeof
with Functions and Objects
const config = {
port: 3000,
host: "localhost",
};
type Config = typeof config; // Inferred type of object
function connect(cfg: Config) {
console.log(`Connecting to ${cfg.host}:${cfg.port}`);
}
📏 9. Using keyof
, Record
, Partial
, Pick
, Omit
These are utility types to check and manipulate types.
type User = {
id: number;
name: string;
email: string;
};
type PartialUser = Partial<User>;
type PickName = Pick<User, "name">;
type WithoutEmail = Omit<User, "email">;
type UserRecord = Record<string, User>;
type UserKeys = keyof User; // "id" | "name" | "email"
🔍 10. Validating JSON or API Responses
✅ Manual Runtime Check Example
type User = { id: number; name: string };
function isUser(data: any): data is User {
return (
typeof data === "object" &&
typeof data.id === "number" &&
typeof data.name === "string"
);
}
✅ With zod
or io-ts
(Schema Validation)
import { z } from "zod";
const UserSchema = z.object({
id: z.number(),
name: z.string(),
});
type User = z.infer<typeof UserSchema>;
const result = UserSchema.safeParse(JSON.parse(json));
if (result.success) {
console.log("Valid user:", result.data);
}
🔥 Use libraries like
zod
,io-ts
, oryup
for runtime validation in production.
🧪 11. Advanced: Exhaustive Checking
Make sure you handle all variants in discriminated unions.
function handle(shape: Shape) {
switch (shape.kind) {
case "circle":
return shape.radius;
case "square":
return shape.size;
default:
const _exhaustiveCheck: never = shape;
return _exhaustiveCheck;
}
}
✅ This triggers a compile-time error if you miss a case.
🛠️ 12. Third-party Runtime Validators (Quick Overview)
Library | Usage | Notes |
---|---|---|
zod |
Runtime validation + TypeScript | Modern and typesafe |
io-ts |
Powerful validation + FP | Functional style |
yup |
Schema-based validation | JS-friendly |
class-validator |
Decorator-based validation | Works with classes |
🧪 13. unknown
vs any
Type | Description |
---|---|
any |
Opt-out of type checking (dangerous) |
unknown |
Safer, forces type narrowing or checks |
function process(input: unknown) {
if (typeof input === "string") {
console.log(input.toUpperCase());
}
}
🪖 14. Type Narrowing in Complex Conditions
function example(value: string | string[] | null) {
if (value && typeof value === "string") {
console.log("It's a string:", value);
} else if (Array.isArray(value)) {
console.log("Array:", value.join(", "));
}
}
💥 Bonus: Type-Safe API Layer
type ApiResponse<T> = {
status: number;
data: T;
error?: string;
};
async function fetchUser(): Promise<ApiResponse<User>> {
const res = await fetch("/api/user");
const json = await res.json();
if (!isUser(json)) {
return { status: 500, data: null as any, error: "Invalid data" };
}
return { status: 200, data: json };
}
📦 Tools & Plugins for Type Safety
- ESLint + TypeScript rules
- tsconfig strict mode
-
ts-node
for dev runtime -
typescript-json-schema
for schema generation
✅ Final Checklist for Type Safety
Task | Technique |
---|---|
Compile-time safety | Type annotations, interfaces, types |
Runtime safety | Type guards, typeof , instanceof
|
Object validation | Schema validators (zod , io-ts , etc.) |
Exhaustive checks | Discriminated unions + never
|
API input/output typing | Generic wrappers, type-safe contracts |
🧠 Conclusion
TypeScript gives you the tools, but it’s up to you to use them wisely.
Think of type checking as both a compile-time shield and a runtime armor. 🛡️
With all these strategies and techniques, you’re now ready to build robust, type-safe applications that prevent bugs and improve maintainability.