Sign In Create Account
Core Principles

TypeScript Core Principles

Fundamental principles that guide effective TypeScript development and type safety.

Embrace Type Safety

TypeScript's primary benefit is catching errors at compile time. Always prefer explicit typing over implicit any.

  • • Enable strict mode in tsconfig.json
  • • Avoid using any type
  • • Use unknown instead of any when type is truly unknown
  • • Prefer type annotations for function parameters and return types

Type Safety Examples

// ✅ Explicit types prevent runtime errors
interface User {
    id: number;
    name: string;
    email: string;
    isActive: boolean;
}

function createUser(userData: Omit<User, 'id'>): User {
    return {
        id: Math.floor(Math.random() * 1000),
        ...userData
    };
}

function getUserById(users: User[], id: number): User | undefined {
    return users.find(user => user.id === id);
}

// ✅ Using unknown for safer type handling
function processApiResponse(data: unknown): User[] {
    if (Array.isArray(data) && data.every(isUser)) {
        return data;
    }
    throw new Error('Invalid user data format');
}

function isUser(value: unknown): value is User {
    return (
        typeof value === 'object' &&
        value !== null &&
        typeof (value as User).id === 'number' &&
        typeof (value as User).name === 'string' &&
        typeof (value as User).email === 'string' &&
        typeof (value as User).isActive === 'boolean'
    );
}

// ❌ Avoid any - loses all type safety
function badFunction(data: any): any {
    return data.someProperty.thatMayNotExist; // Runtime error waiting to happen
}

// ❌ Implicit any (when noImplicitAny is false)
function calculateTotal(items) { // Parameter implicitly has 'any' type
    return items.reduce((sum, item) => sum + item.price, 0);
}

Strict Mode Benefits

  • noImplicitAny: Requires explicit type annotations
  • strictNullChecks: Prevents null/undefined errors
  • strictFunctionTypes: Better function type checking
  • noImplicitReturns: Ensures all code paths return values

Interface vs Type Aliases

Choose interfaces for object shapes that might be extended, and type aliases for unions, primitives, and computed types.

Interface vs Type Usage

// ✅ Use interfaces for object shapes and extensibility
interface BaseEntity {
    id: string;
    createdAt: Date;
    updatedAt: Date;
}

interface User extends BaseEntity {
    name: string;
    email: string;
    role: UserRole;
}

interface AdminUser extends User {
    permissions: Permission[];
    lastLogin?: Date;
}

// ✅ Use type aliases for unions and computed types
type UserRole = 'admin' | 'user' | 'guest';
type Permission = 'read' | 'write' | 'delete';

type ApiResponse<T> = {
    data: T;
    status: 'success' | 'error';
    message?: string;
};

type UserResponse = ApiResponse<User>;
type UsersResponse = ApiResponse<User[]>;

// ✅ Use type aliases for conditional types
type NonNullable<T> = T extends null | undefined ? never : T;
type UserEmail = NonNullable<User['email']>;

// ✅ Interface declaration merging (useful for extending third-party types)
interface Window {
    customProperty: string;
}

declare global {
    interface Window {
        gtag: (command: string, ...args: any[]) => void;
    }
}

// ❌ Don't use interfaces for primitive unions
interface Status { // Should be a type alias
    status: 'loading' | 'success' | 'error';
}

// ✅ Better as type alias
type Status = 'loading' | 'success' | 'error';

Null Safety and Optional Properties

Handle null and undefined values explicitly to prevent runtime errors.

Null Safety Examples

// ✅ Explicit null handling
interface UserProfile {
    id: string;
    name: string;
    email: string;
    avatar?: string; // Optional property
    lastLogin: Date | null; // Explicit null
}

function formatUserDisplay(user: UserProfile): string {
    const avatar = user.avatar ?? '/default-avatar.png';
    const lastLoginText = user.lastLogin 
        ? `Last login: ${user.lastLogin.toLocaleDateString()}`
        : 'Never logged in';
    
    return `${user.name} (${lastLoginText})`;
}

// ✅ Type guards for null checking
function isValidEmail(email: string | null | undefined): email is string {
    return email != null && email.includes('@');
}

function processUser(user: UserProfile): void {
    if (isValidEmail(user.email)) {
        sendEmail(user.email); // TypeScript knows email is string here
    }
}

// ✅ Optional chaining and nullish coalescing
function getUserDisplayName(user: UserProfile | null): string {
    return user?.name ?? 'Anonymous User';
}

function getNestedProperty(obj: any): string {
    return obj?.data?.user?.profile?.displayName ?? 'N/A';
}

// ✅ Strict null checks prevent common errors
function calculateLength(items: string[] | null): number {
    if (items === null) {
        return 0;
    }
    return items.length; // TypeScript knows items is not null here
}

// ❌ Avoid non-null assertion unless absolutely certain
function riskyFunction(user: UserProfile): string {
    return user.avatar!.toUpperCase(); // Could throw if avatar is undefined
}

// ✅ Better - handle the null case
function safeFunction(user: UserProfile): string {
    if (!user.avatar) {
        throw new Error('User avatar is required');
    }
    return user.avatar.toUpperCase();
}

Type Guards and Assertions

Use type guards to safely narrow types and avoid unsafe type assertions.

Type Guards and Assertions

// ✅ User-defined type guards
function isString(value: unknown): value is string {
    return typeof value === 'string';
}

function isNumber(value: unknown): value is number {
    return typeof value === 'number' && !isNaN(value);
}

function isUser(obj: unknown): obj is User {
    return (
        typeof obj === 'object' &&
        obj !== null &&
        'id' in obj &&
        'name' in obj &&
        'email' in obj
    );
}

// ✅ Using type guards
function processUserData(data: unknown): User {
    if (isUser(data)) {
        return data; // TypeScript knows this is User
    }
    throw new Error('Invalid user data');
}

// ✅ Discriminated unions with type guards
interface LoadingState {
    status: 'loading';
}

interface SuccessState {
    status: 'success';
    data: User[];
}

interface ErrorState {
    status: 'error';
    message: string;
}

type AppState = LoadingState | SuccessState | ErrorState;

function handleState(state: AppState): string {
    switch (state.status) {
        case 'loading':
            return 'Loading...';
        case 'success':
            return `Loaded ${state.data.length} users`; // TypeScript knows about data
        case 'error':
            return `Error: ${state.message}`; // TypeScript knows about message
        default:
            // TypeScript will error if we don't handle all cases
            const exhaustiveCheck: never = state;
            return exhaustiveCheck;
    }
}

// ✅ Safe type assertion with validation
function assertIsUser(obj: unknown): asserts obj is User {
    if (!isUser(obj)) {
        throw new Error('Expected User object');
    }
}

function processApiUser(data: unknown): void {
    assertIsUser(data);
    // data is now typed as User
    console.log(data.name);
}

// ❌ Dangerous type assertions without validation
function dangerousAssertion(data: unknown): User {
    return data as User; // No runtime validation - could crash
}

// ❌ Overusing type assertions
function badExample(element: HTMLElement): void {
    const input = element as HTMLInputElement; // What if it's not an input?
    input.value = 'test'; // Could throw at runtime
}

// ✅ Better - check the element type
function goodExample(element: HTMLElement): void {
    if (element instanceof HTMLInputElement) {
        element.value = 'test'; // Safe - TypeScript knows it's an input
    }
}

Type Assertion Guidelines

  • • Use type guards instead of type assertions when possible
  • • Only use as when you're certain about the type
  • • Consider using assertion functions for validation
  • • Avoid any and as any casting