TypeScript Beyond Basics


TypeScript enhances JavaScript by adding powerful type features, enabling developers to write safer, more predictable, and maintainable code using arrow functions. By enforcing type checks at compile time, it helps catch errors early, improves code clarity, and makes large-scale applications easier to manage.

1. Advanced Types and Generics

TypeScript allows you to define types dynamically and flexibly, making your code more readable and less error-prone.

Example: Generics in a function

const wrapInArray = <T>(value: T): T[] => [value];

const numbers = wrapInArray(42); // number[]
const strings = wrapInArray("hello"); // string[]

Advanced Types: Union, Intersection, Conditional

type Success<T> = { data: T; success: true };
type Failure = { error: string; success: false };

type Result<T> = Success<T> | Failure;

const handleResult = <T>(result: Result<T>) => {
  if (result.success) {
    console.log(result.data);
  } else {
    console.error(result.error);
  }
};

Difference between any and unknown

  • any: disables type checking, can be assigned to/from any type, unsafe.
  • unknown: safer alternative, requires type checking before using.
let valueAny: any = 10;
let valueUnknown: unknown = 10;

// OK
const num1: number = valueAny;

// Error, must check type first
if (typeof valueUnknown === 'number') {
  const num2: number = valueUnknown;
}

Creating constants in TypeScript

const API_URL: string = 'https://api.example.com';
const MAX_RETRIES: number = 5;
  • Use const for values that should not change.
  • Optionally, type annotations improve clarity.

2. Type-Safe APIs and Backend Integration

One of the strongest points of TypeScript is type safety across your API calls. You can ensure that your frontend and backend always agree on the data structure.

Example: Fetching typed API data

interface User { id: number; name: string; email: string; }

const fetchUsers = async (): Promise<User[]> => {
  const response = await fetch('/api/users');
  const data = await response.json();
  return data as User[];
};

fetchUsers().then(users => console.log(users));

Benefits:

  • Avoid runtime type errors
  • Auto-completion in editors
  • Easier refactoring

3. Practical Design Patterns in TypeScript

TypeScript works well with classic design patterns. Using interfaces, generics, and advanced types allows you to implement these patterns safely.

Singleton Example:

const Logger = (() => {
  let instance: { log: (msg: string) => void } | null = null;

  const createInstance = () => ({ log: (message: string) => console.log(message) });

  return {
    getInstance: () => {
      if (!instance) instance = createInstance();
      return instance;
    }
  };
})();

const logger = Logger.getInstance();
logger.log("App started");

Observer Pattern Example:

interface Observer { update: (data: any) => void; }

const Subject = () => {
  const observers: Observer[] = [];

  const subscribe = (observer: Observer) => observers.push(observer);
  const notify = (data: any) => observers.forEach(o => o.update(data));

  return { subscribe, notify };
};

Why it matters:

  • Enforces structure in large applications
  • Reduces bugs and duplication
  • Combines the power of TypeScript types with proven design patterns

4. Utility Types and Practical Usage

TypeScript comes with several utility types that help you write more concise and safe code.

Partial: Makes all properties of a type optional.

interface User { id: number; name: string; email: string; }
const updateUser = (user: Partial<User>) => { /* ... */ };

Required: Makes all properties required.

interface Config { apiUrl?: string; retries?: number; }
const initConfig = (config: Required<Config>) => { /* ... */ };

Record: Creates an object type with specific keys and value types.

const userRoles: Record<string, string> = {
  admin: 'all-access',
  guest: 'read-only'
};

Using these utility types improves readability, type safety, and reduces boilerplate in your code.

5. Integrating TypeScript with Frontend Frameworks

Using TypeScript with frameworks like React or Vue ensures type safety across components and props.

React Example:

interface ButtonProps { label: string; onClick: () => void; }
const Button = ({ label, onClick }: ButtonProps) => (
  <button onClick={onClick}>{label}</button>
);

Vue 3 Example (Composition API):

import { defineComponent, ref } from 'vue';

const Counter = defineComponent({
  setup: () => {
    const count = ref<number>(0);
    const increment = () => count.value++;
    return { count, increment };
  }
});

Benefits:

  • Catch errors at compile time
  • Better auto-completion and developer experience
  • Easier refactoring and maintenance

Conclusion

Going beyond basic TypeScript allows you to write robust, maintainable, and scalable code. Leveraging advanced types, generics, type-safe APIs, constants, utility types, design patterns, and integration with frontend frameworks using arrow functions exclusively improves both developer productivity and code quality.

Bartłomiej Nowak

Bartłomiej Nowak

Programmer

Programmer focused on performance, simplicity, and good architecture. I enjoy working with modern JavaScript, TypeScript, and backend logic — building tools that scale and make sense.

Recent Posts