Type Safety in TypeScript
Leverage TypeScript's type system for safer, more maintainable code.
By EMEPublished: February 20, 2025
typescripttypestype safetygenericsinterfaces
A Simple Analogy
Types in TypeScript are like contract agreements. They specify what's promised and catch violations at compile-time.
Why Type Safety?
- Fewer bugs: Errors at compile-time
- Better IDE support: Autocomplete and suggestions
- Documentation: Types document intent
- Refactoring: Safe code changes
- Maintainability: Easier to understand
Basic Types
// Primitives
const name: string = "Alice";
const age: number = 30;
const active: boolean = true;
const nothing: null = null;
const unknown: undefined = undefined;
// Collections
const numbers: number[] = [1, 2, 3];
const mixed: (string | number)[] = [1, "two"];
const tuple: [string, number] = ["age", 30];
// Objects
interface User {
id: number;
name: string;
email?: string; // Optional
readonly createdAt: Date; // Readonly
}
const user: User = {
id: 1,
name: "Bob",
createdAt: new Date()
};
Generics
// Generic function
function getFirst<T>(items: T[]): T {
return items[0];
}
const firstNumber = getFirst([1, 2, 3]); // number
const firstString = getFirst(["a", "b"]); // string
// Generic interface
interface Container<T> {
value: T;
getValue(): T;
}
const numberContainer: Container<number> = {
value: 42,
getValue() { return this.value; }
};
// Generic constraints
function merge<T extends object, U extends object>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
const merged = merge({ a: 1 }, { b: 2 }); // { a: 1; b: 2 }
Union and Intersection Types
// Union: one of multiple types
type Status = "pending" | "completed" | "failed";
type ID = string | number;
function processStatus(status: Status) {
switch (status) {
case "pending": return "Waiting...";
case "completed": return "Done!";
case "failed": return "Error";
}
}
// Intersection: combination of types
interface Timestamped {
createdAt: Date;
updatedAt: Date;
}
interface Named {
name: string;
}
type Document = Timestamped & Named;
const doc: Document = {
name: "Report",
createdAt: new Date(),
updatedAt: new Date()
};
Utility Types
// Partial: all properties optional
type PartialUser = Partial<User>;
// Required: all properties required
type RequiredUser = Required<User>;
// Readonly: all properties readonly
type ReadonlyUser = Readonly<User>;
// Pick: select specific properties
type UserPreview = Pick<User, "id" | "name">;
// Omit: exclude specific properties
type UserWithoutEmail = Omit<User, "email">;
// Record: object with specific keys
type Role = "admin" | "user" | "guest";
const permissions: Record<Role, boolean> = {
admin: true,
user: true,
guest: false
};
// Exclude: remove types from union
type NonString = Exclude<string | number | boolean, string>; // number | boolean
Type Guards
// Type predicate
function isUser(obj: unknown): obj is User {
return (
typeof obj === "object" &&
obj !== null &&
"id" in obj &&
"name" in obj &&
typeof obj.id === "number" &&
typeof obj.name === "string"
);
}
// Usage
const data: unknown = JSON.parse(jsonString);
if (isUser(data)) {
console.log(data.name); // Safe: TypeScript knows it's User
}
// instanceof guard
if (error instanceof RangeError) {
console.log("Range error occurred");
}
// Discriminated unions
type Result =
| { status: "success"; data: string }
| { status: "error"; error: Error };
function handleResult(result: Result) {
if (result.status === "success") {
console.log(result.data);
} else {
console.log(result.error);
}
}
Best Practices
- Avoid
any: Use specific types - Enable strict mode: Catch more errors
- Use type guards: Safe type narrowing
- Document with types: Types are documentation
- Leverage inference: Let TypeScript infer when possible
Related Concepts
- Advanced generics
- Conditional types
- Mapped types
- Type inference
Summary
TypeScript's type system prevents entire categories of bugs at compile-time. Use interfaces, generics, and utility types to build robust applications.