TypeScript 5.5 Advanced Features: Type Safety na sterydach

2025-10-18#typescript#type-safety#advanced

TypeScript 5.5 Advanced Features: Type Safety na sterydach

TypeScript przestał być "nice to have" - w 2025 roku to industry standard. 87% nowych projektów JavaScript używa TypeScript (Stack Overflow Survey 2025), a firmy jak Airbnb, Slack i Shopify przepisały miliony linii kodu z JavaScript na TS.

Dlaczego? Jeden prosty powód: błędy wykrywane w compile-time, nie w production.

W tym przewodniku pokażę Ci zaawansowane features TypeScript 5.5, których większość developerów nie używa - a powinni. Template literal types, const assertions, branded types, discriminated unions i wiele więcej.

Spis treści

  1. Template Literal Types - dynamiczne typy ze stringów
  2. Const Assertions - immutability bez 'as const'
  3. Branded Types - runtime safety w compile time
  4. Discriminated Unions - type-safe state machines
  5. Conditional Types - if/else dla typów
  6. infer keyword - type extraction magic
  7. Mapped Types - DRY principle dla typów

1. Template Literal Types - Dynamiczne typy ze stringów

Problem: Jak stypować CSS classes?

// ❌ ZŁE: Any string allowed
function applyClass(className: string) {
  element.classList.add(className);
}

applyClass('bg-red-500'); // ✅ OK
applyClass('bg-red-5000'); // ❌ Typo, but no error!

✅ Solution: Template Literal Types

type Color = 'red' | 'blue' | 'green' | 'yellow';
type Shade = '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900';
type TailwindColor = `bg-${Color}-${Shade}`;

function applyClass(className: TailwindColor) {
  element.classList.add(className);
}

applyClass('bg-red-500'); // ✅ OK
applyClass('bg-red-5000'); // ❌ Compile error!

Result: TypeScript autocomplete pokazuje wszystkie 36 możliwych kombinacji! 🎯

Real-world Example: API Route Types

type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type APIVersion = 'v1' | 'v2';
type Resource = 'users' | 'posts' | 'comments';

type APIRoute = `/${APIVersion}/${Resource}`;
// Result: "/v1/users" | "/v1/posts" | "/v1/comments" | "/v2/users" | ...

type APIEndpoint = `${HTTPMethod} ${APIRoute}`;
// Result: "GET /v1/users" | "POST /v1/users" | "DELETE /v2/comments" | ...

function callAPI(endpoint: APIEndpoint) {
  const [method, route] = endpoint.split(' ');
  // TypeScript knows method is HTTPMethod and route is APIRoute!
}

callAPI('GET /v1/users'); // ✅ OK
callAPI('PATCH /v1/users'); // ❌ Error: PATCH nie jest w HTTPMethod

2. Const Assertions - Immutability bez 'as const'

Problem: Mutable types by default

const config = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  retries: 3,
};

// TypeScript infers:
// {
//   apiUrl: string;  ← Too broad!
//   timeout: number; ← Too broad!
//   retries: number; ← Too broad!
// }

config.apiUrl = 'https://hack.me'; // ❌ No error, but dangerous!

✅ Solution: as const assertion

const config = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  retries: 3,
} as const;

// TypeScript infers:
// {
//   readonly apiUrl: "https://api.example.com"; ← Exact type!
//   readonly timeout: 5000;                     ← Exact type!
//   readonly retries: 3;                        ← Exact type!
// }

config.apiUrl = 'https://hack.me'; // ✅ Compile error: cannot assign to readonly!

Advanced: Const assertion dla arrays

// ❌ ZŁE: Mutable array
const colors = ['red', 'blue', 'green'];
// Type: string[] ← Any string can be added/removed

colors.push('purple'); // No error
colors[0] = 'yellow'; // No error

// ✅ DOBRE: Immutable tuple
const colors = ['red', 'blue', 'green'] as const;
// Type: readonly ["red", "blue", "green"] ← Exact, immutable tuple

colors.push('purple'); // ❌ Error: push doesn't exist on readonly array
colors[0] = 'yellow'; // ❌ Error: cannot assign to readonly

Real-world: Type-safe enum alternative

// Instead of enum (which compiles to JS)
enum Status {
  Pending,
  Active,
  Completed
}

// Use const assertion (zero JS output!)
const Status = {
  Pending: 'pending',
  Active: 'active',
  Completed: 'completed',
} as const;

type Status = typeof Status[keyof typeof Status];
// Type: "pending" | "active" | "completed"

function updateStatus(status: Status) {
  // TypeScript autocomplete works perfectly!
}

updateStatus(Status.Active); // ✅ OK
updateStatus('active'); // ✅ OK (string literal)
updateStatus('invalid'); // ❌ Error

3. Branded Types - Runtime Safety w Compile Time

Problem: Structural typing może być za luźne

type UserId = string;
type ProductId = string;

function getUser(id: UserId) { /* ... */ }
function getProduct(id: ProductId) { /* ... */ }

const userId: UserId = 'user_123';
const productId: ProductId = 'prod_456';

getUser(productId); // ❌ Should error, but doesn't! (both are strings)

✅ Solution: Branded Types (Nominal Typing)

type Brand<K, T> = K & { __brand: T };

type UserId = Brand<string, 'UserId'>;
type ProductId = Brand<string, 'ProductId'>;

function getUser(id: UserId) { /* ... */ }
function getProduct(id: ProductId) { /* ... */ }

// Smart constructor
function createUserId(id: string): UserId {
  if (!id.startsWith('user_')) throw new Error('Invalid UserId');
  return id as UserId;
}

const userId = createUserId('user_123');
const productId = 'prod_456' as ProductId;

getUser(userId); // ✅ OK
getUser(productId); // ❌ Compile error! ProductId !== UserId

Advanced: Email, URL, NonEmptyString validation

type Email = Brand<string, 'Email'>;
type URL = Brand<string, 'URL'>;
type NonEmptyString = Brand<string, 'NonEmptyString'>;

function createEmail(value: string): Email {
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
    throw new Error('Invalid email');
  }
  return value as Email;
}

function createURL(value: string): URL {
  try {
    new URL(value);
    return value as URL;
  } catch {
    throw new Error('Invalid URL');
  }
}

function createNonEmptyString(value: string): NonEmptyString {
  if (value.trim().length === 0) {
    throw new Error('String cannot be empty');
  }
  return value as NonEmptyString;
}

// Usage
function sendEmail(to: Email, body: NonEmptyString) {
  // TypeScript guarantees 'to' is valid email and 'body' is not empty!
}

sendEmail(
  createEmail('user@example.com'), 
  createNonEmptyString('Hello!')
); // ✅ OK

sendEmail('invalid-email', ''); // ❌ Compile errors on both args!

4. Discriminated Unions - Type-safe State Machines

Problem: Union types bez discriminatora

type Response = {
  loading: boolean;
  data?: User;
  error?: string;
};

function handleResponse(response: Response) {
  if (response.loading) {
    // ⚠️ TypeScript doesn't know data/error are undefined here
    console.log(response.data?.name); // Might be undefined!
  }
}

✅ Solution: Discriminated Union

type Response =
  | { status: 'loading' }
  | { status: 'success'; data: User }
  | { status: 'error'; error: string };

function handleResponse(response: Response) {
  switch (response.status) {
    case 'loading':
      // TypeScript knows: only 'status' exists here
      console.log('Loading...');
      break;
      
    case 'success':
      // TypeScript knows: 'data' exists and 'error' doesn't
      console.log(response.data.name); // ✅ No optional chaining needed!
      break;
      
    case 'error':
      // TypeScript knows: 'error' exists and 'data' doesn't
      console.log(response.error); // ✅ Safe!
      break;
  }
}

Real-world: Form validation state

type FormState =
  | { status: 'idle' }
  | { status: 'validating' }
  | { status: 'valid'; values: FormValues }
  | { status: 'invalid'; errors: Record<string, string> }
  | { status: 'submitting'; values: FormValues }
  | { status: 'submitted'; result: SubmitResult };

function FormComponent({ state }: { state: FormState }) {
  switch (state.status) {
    case 'idle':
      return <button>Start</button>;
      
    case 'validating':
      return <Spinner />;
      
    case 'valid':
      return <button onClick={() => submit(state.values)}>Submit</button>;
      
    case 'invalid':
      return <ErrorList errors={state.errors} />;
      
    case 'submitting':
      return <button disabled>Submitting...</button>;
      
    case 'submitted':
      return <Success result={state.result} />;
  }
}

Benefit: Impossible states are impossible! Nie możesz mieć loading: true i data: User jednocześnie.


5. Conditional Types - If/Else dla typów

Basic Syntax

type IsString<T> = T extends string ? true : false;

type A = IsString<string>; // true
type B = IsString<number>; // false

Real-world: Extract function return types

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function getUser() {
  return { id: 1, name: 'John' };
}

type User = ReturnType<typeof getUser>;
// Type: { id: number; name: string; }

Advanced: Flatten array types

type Flatten<T> = T extends Array<infer U> ? U : T;

type A = Flatten<string[]>;   // string
type B = Flatten<number[]>;   // number
type C = Flatten<string>;     // string (not array)

Utility: DeepPartial (recursive)

type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

interface User {
  id: number;
  profile: {
    name: string;
    address: {
      street: string;
      city: string;
    };
  };
}

type PartialUser = DeepPartial<User>;
// All nested properties are optional!

const user: PartialUser = {
  profile: {
    address: {
      city: 'NYC' // Only city, all others optional!
    }
  }
};

6. infer keyword - Type Extraction Magic

Extract Promise resolved type

type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type A = UnwrapPromise<Promise<string>>; // string
type B = UnwrapPromise<Promise<number>>; // number
type C = UnwrapPromise<boolean>;         // boolean

Extract function parameters

type Parameters<T> = T extends (...args: infer P) => any ? P : never;

function example(a: string, b: number, c: boolean) {}

type Params = Parameters<typeof example>;
// Type: [string, number, boolean]

Extract React component props

type ComponentProps<T> = T extends React.FC<infer P> ? P : never;

const Button: React.FC<{ label: string; onClick: () => void }> = ({ label, onClick }) => (
  <button onClick={onClick}>{label}</button>
);

type ButtonProps = ComponentProps<typeof Button>;
// Type: { label: string; onClick: () => void }

7. Mapped Types - DRY Principle dla typów

Utility: Make all properties required

type Required<T> = {
  [P in keyof T]-?: T[P];
};

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

type RequiredUser = Required<User>;
// Type: { id: number; name: string; email: string; } ← All required!

Utility: Make all properties readonly

type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

type ReadonlyUser = Readonly<User>;
// All properties are readonly

Advanced: Getters type from object

type Getters<T> = {
  [P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};

interface State {
  count: number;
  name: string;
}

type StateGetters = Getters<State>;
// Type: {
//   getCount: () => number;
//   getName: () => string;
// }

Podsumowanie: TypeScript Best Practices 2025

✅ DO:

  • Template literal types dla string unions
  • Const assertions dla config objects
  • Branded types dla critical data (IDs, emails)
  • Discriminated unions dla state machines
  • Conditional types dla utility types
  • infer do type extraction
  • Mapped types dla transformations

❌ DON'T:

  • any type (use unknown instead)
  • Type assertions (as) bez validacji
  • Enums (use const objects + as const)
  • @ts-ignore (fix the root cause!)
  • Explicit return types gdy TypeScript infers correctly

🎯 ROI:

  • 40% mniej runtime errors (Microsoft Research)
  • 15% szybszy code review (less mental overhead)
  • Better refactoring (rename/extract with confidence)

Powiązane artykuły


Potrzebujesz pomocy z TypeScript w projekcie? Skontaktuj się ze mną - migruję legacy JS codebases do type-safe TypeScript!


Autor: Next Gen Code | Data publikacji: 18 października 2025 | Czas czytania: 11 minut

TypeScript 5.5 Advanced Features: Type Safety na sterydach - NextGenCode Blog | NextGenCode