Przejdź do treści
Technologie

TypeScript Generics - Zaawansowane typy w praktyce

Opublikowano:
·3 min czytania·Autor: MDS Software Solutions Group

TypeScript Generics Zaawansowane

technologie

TypeScript Generics - Zaawansowane typy w praktyce

Generics to jeden z najpotężniejszych mechanizmów TypeScript, pozwalający tworzyć wielokrotnie używalne, typowo-bezpieczne komponenty. W tym artykule zagłębimy się w zaawansowane techniki pracy z generics - od podstawowych funkcji generycznych po skomplikowane wzorce projektowe stosowane w produkcyjnych aplikacjach.

Funkcje generyczne#

Funkcje generyczne pozwalają pisać kod, który działa z różnymi typami, zachowując pełne bezpieczeństwo typów. Zamiast używać any, definiujemy parametr typu, który TypeScript inferuje na podstawie przekazanych argumentów.

// Podstawowa funkcja generyczna
function identity<T>(value: T): T {
  return value;
}

const str = identity("hello"); // typ: string
const num = identity(42);      // typ: number

// Funkcja z wieloma parametrami typu
function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}

const result = pair("name", 42); // typ: [string, number]

// Generyczna funkcja z callback
function map<T, U>(array: T[], fn: (item: T, index: number) => U): U[] {
  return array.map(fn);
}

const lengths = map(["hello", "world"], (s) => s.length); // typ: number[]

Funkcje generyczne z domyślnymi typami ułatwiają korzystanie z API, gdy nie potrzebujemy specyficznego typu:

function createState<T = string>(initial: T): {
  get: () => T;
  set: (value: T) => void;
} {
  let state = initial;
  return {
    get: () => state,
    set: (value: T) => { state = value; },
  };
}

const stringState = createState("hello");   // T = string (inferred)
const numberState = createState<number>(0); // T = number (explicit)

Klasy generyczne#

Klasy generyczne pozwalają tworzyć struktury danych i serwisy, które działają z dowolnym typem, jednocześnie zapewniając pełną kontrolę nad typami.

class Stack<T> {
  private items: T[] = [];

  push(item: T): void {
    this.items.push(item);
  }

  pop(): T | undefined {
    return this.items.pop();
  }

  peek(): T | undefined {
    return this.items[this.items.length - 1];
  }

  get size(): number {
    return this.items.length;
  }
}

const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
const top = numberStack.peek(); // typ: number | undefined

// Generyczna klasa z wieloma parametrami
class KeyValueStore<K extends string | number, V> {
  private store = new Map<K, V>();

  set(key: K, value: V): void {
    this.store.set(key, value);
  }

  get(key: K): V | undefined {
    return this.store.get(key);
  }

  entries(): [K, V][] {
    return Array.from(this.store.entries());
  }
}

const userStore = new KeyValueStore<string, { name: string; age: number }>();
userStore.set("user1", { name: "Jan", age: 30 });

Interfejsy generyczne#

Interfejsy generyczne definiują kontrakty, które mogą być parametryzowane typami. Są fundamentem wielu wzorców projektowych w TypeScript.

// Generyczny interfejs odpowiedzi API
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
  timestamp: Date;
}

// Generyczny interfejs z paginacją
interface PaginatedResponse<T> {
  items: T[];
  total: number;
  page: number;
  pageSize: number;
  hasNext: boolean;
  hasPrevious: boolean;
}

// Użycie
interface User {
  id: string;
  name: string;
  email: string;
}

type UserResponse = ApiResponse<User>;
type UserListResponse = ApiResponse<PaginatedResponse<User>>;

// Generyczny interfejs z metodami
interface Comparable<T> {
  compareTo(other: T): number;
  equals(other: T): boolean;
}

class Money implements Comparable<Money> {
  constructor(
    public amount: number,
    public currency: string
  ) {}

  compareTo(other: Money): number {
    if (this.currency !== other.currency) {
      throw new Error("Cannot compare different currencies");
    }
    return this.amount - other.amount;
  }

  equals(other: Money): boolean {
    return this.amount === other.amount && this.currency === other.currency;
  }
}

Ograniczenia typów (extends)#

Słowo kluczowe extends w kontekście generics pozwala nałożyć ograniczenia na parametry typu, zapewniając, że przekazany typ spełnia określone wymagania.

// Ograniczenie do obiektów z polem length
function logLength<T extends { length: number }>(item: T): T {
  console.log(`Length: ${item.length}`);
  return item;
}

logLength("hello");      // OK - string ma length
logLength([1, 2, 3]);    // OK - array ma length
// logLength(42);         // Błąd - number nie ma length

// Ograniczenie z keyof
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: "Jan", age: 30, email: "jan@example.com" };
const name = getProperty(user, "name"); // typ: string
const age = getProperty(user, "age");   // typ: number
// getProperty(user, "phone");          // Błąd - "phone" nie istnieje w User

// Ograniczenie z wieloma constraints
interface HasId {
  id: string;
}

interface HasTimestamps {
  createdAt: Date;
  updatedAt: Date;
}

function updateEntity<T extends HasId & HasTimestamps>(
  entity: T,
  updates: Partial<Omit<T, "id" | "createdAt">>
): T {
  return {
    ...entity,
    ...updates,
    updatedAt: new Date(),
  };
}

Typy warunkowe (Conditional Types)#

Conditional types pozwalają tworzyć typy, które zależą od warunków. Działają podobnie do operatora ternary, ale na poziomie systemu typów.

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

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

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

type C = UnwrapPromise<Promise<string>>; // string
type D = UnwrapPromise<number>;          // number

// Zagnieżdżone conditional types
type DeepUnwrap<T> = T extends Promise<infer U>
  ? DeepUnwrap<U>
  : T;

type E = DeepUnwrap<Promise<Promise<Promise<string>>>>; // string

// Distributive conditional types
type NonNullable<T> = T extends null | undefined ? never : T;

type F = NonNullable<string | null | undefined>; // string

// Praktyczny przykład - wyciąganie typów z funkcji
type FunctionReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type G = FunctionReturnType<() => string>;           // string
type H = FunctionReturnType<(x: number) => boolean>; // boolean

Mapped Types#

Mapped types pozwalają transformować istniejące typy, iterując po ich kluczach. To potężne narzędzie do tworzenia wariantów typów.

// Podstawowy mapped type - wszystkie pola opcjonalne
type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

// Wszystkie pola readonly
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

// Zaawansowany mapped type - nullable fields
type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

// Mapped type z modyfikatorem -
type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};

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

// Mapped type z remapowaniem kluczy (as)
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type Setters<T> = {
  [K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};

interface Person {
  name: string;
  age: number;
}

type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; }

type PersonSetters = Setters<Person>;
// { setName: (value: string) => void; setAge: (value: number) => void; }

// Filtrowanie kluczy w mapped types
type OnlyStrings<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};

interface Mixed {
  name: string;
  age: number;
  email: string;
  active: boolean;
}

type StringFields = OnlyStrings<Mixed>;
// { name: string; email: string; }

Template Literal Types#

Template literal types pozwalają tworzyć typy na podstawie szablonów stringów, co jest szczególnie przydatne przy definiowaniu API i event handlerów.

// Podstawowe template literal types
type EventName = `on${Capitalize<"click" | "focus" | "blur">}`;
// "onClick" | "onFocus" | "onBlur"

// Dynamiczne generowanie nazw eventów
type DOMEvents = "click" | "mouseover" | "mouseout" | "keydown" | "keyup";
type EventHandler = `on${Capitalize<DOMEvents>}`;

// Template literal z generics
type PropEventType<T extends string> = `${T}Changed`;

type UserEvents = PropEventType<"name" | "email" | "age">;
// "nameChanged" | "emailChanged" | "ageChanged"

// Zaawansowane - parsowanie template literals
type ExtractRouteParams<T extends string> =
  T extends `${infer _}:${infer Param}/${infer Rest}`
    ? Param | ExtractRouteParams<`/${Rest}`>
    : T extends `${infer _}:${infer Param}`
      ? Param
      : never;

type Params = ExtractRouteParams<"/users/:userId/posts/:postId">;
// "userId" | "postId"

// CSS Unit types
type CSSUnit = "px" | "em" | "rem" | "vh" | "vw" | "%";
type CSSValue = `${number}${CSSUnit}`;

const width: CSSValue = "100px";    // OK
const height: CSSValue = "50vh";    // OK
// const bad: CSSValue = "abc";     // Błąd

Słowo kluczowe infer#

Słowo kluczowe infer pozwala wyciągać typy z wewnątrz innych typów. Jest dostępne wyłącznie wewnątrz conditional types i stanowi jedno z najpotężniejszych narzędzi systemu typów TypeScript.

// Wyciąganie typu elementu z tablicy
type ElementType<T> = T extends (infer U)[] ? U : never;

type I = ElementType<string[]>;  // string
type J = ElementType<number[]>;  // number

// Wyciąganie typu argumentów funkcji
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;

type K = FirstArg<(name: string, age: number) => void>; // string

// Wyciąganie typu z Promise
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;

// Wyciąganie propów z komponentu React
type PropsOf<T> = T extends React.ComponentType<infer P> ? P : never;

// Wyciąganie typu zwracanego z async funkcji
type AsyncReturnType<T extends (...args: any[]) => Promise<any>> =
  T extends (...args: any[]) => Promise<infer R> ? R : never;

type UserData = AsyncReturnType<() => Promise<{ id: string; name: string }>>;
// { id: string; name: string }

// Infer z template literal types
type ParseQueryString<T extends string> =
  T extends `${infer Key}=${infer Value}&${infer Rest}`
    ? { [K in Key]: Value } & ParseQueryString<Rest>
    : T extends `${infer Key}=${infer Value}`
      ? { [K in Key]: Value }
      : {};

Utility Types - doglebna analiza#

TypeScript dostarcza bogaty zestaw wbudowanych utility types. Zrozumienie ich implementacji pozwala tworzyć własne, zaawansowane typy.

// Partial<T> - wszystkie pola opcjonalne
type MyPartial<T> = { [K in keyof T]?: T[K] };

// Required<T> - wszystkie pola wymagane
type MyRequired<T> = { [K in keyof T]-?: T[K] };

// Pick<T, K> - wybierz podzbiór kluczy
type MyPick<T, K extends keyof T> = { [P in K]: T[P] };

// Omit<T, K> - pomiń wybrane klucze
type MyOmit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

// Record<K, V> - typ z kluczami K i wartościami V
type MyRecord<K extends keyof any, V> = { [P in K]: V };

// Extract<T, U> - wyciągnij typy z unii pasujące do U
type MyExtract<T, U> = T extends U ? T : never;

// Exclude<T, U> - wyklucz typy z unii pasujące do U
type MyExclude<T, U> = T extends U ? never : T;

// ReturnType<T> - typ zwracany przez funkcję
type MyReturnType<T extends (...args: any[]) => any> =
  T extends (...args: any[]) => infer R ? R : any;

// Parameters<T> - typ parametrów funkcji jako tuple
type MyParameters<T extends (...args: any[]) => any> =
  T extends (...args: infer P) => any ? P : never;

Praktyczne zastosowania utility types:

interface Product {
  id: string;
  name: string;
  price: number;
  description: string;
  category: string;
  inStock: boolean;
  createdAt: Date;
}

// Typ do tworzenia nowego produktu (bez auto-generowanych pól)
type CreateProduct = Omit<Product, "id" | "createdAt">;

// Typ do aktualizacji produktu (wszystko opcjonalne)
type UpdateProduct = Partial<Omit<Product, "id">>;

// Typ do wyświetlania na liście
type ProductListItem = Pick<Product, "id" | "name" | "price" | "inStock">;

// Mapa kategorii do produktów
type ProductsByCategory = Record<string, Product[]>;

// Wyciągnij tylko stringowe klucze
type StringKeys = Extract<keyof Product, string>;

// Typy eventów
type ProductEvent = "created" | "updated" | "deleted" | "archived";
type ActiveEvent = Exclude<ProductEvent, "archived">;
// "created" | "updated" | "deleted"

Branded Types#

Branded types (typy markowane) pozwalają tworzyć nominalne typy w strukturalnym systemie typów TypeScript. Zapobiegają pomyłkowemu przekazaniu wartości o tym samym typie bazowym, ale różnym znaczeniu semantycznym.

// Definicja branded type
type Brand<T, B extends string> = T & { readonly __brand: B };

type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
type Email = Brand<string, "Email">;
type USD = Brand<number, "USD">;
type EUR = Brand<number, "EUR">;

// Funkcje tworzące (smart constructors)
function createUserId(id: string): UserId {
  if (!id.match(/^usr_[a-zA-Z0-9]+$/)) {
    throw new Error("Invalid user ID format");
  }
  return id as UserId;
}

function createEmail(email: string): Email {
  if (!email.includes("@")) {
    throw new Error("Invalid email format");
  }
  return email as Email;
}

function createUSD(amount: number): USD {
  return Math.round(amount * 100) / 100 as USD;
}

// Teraz TypeScript chroni przed pomyłkami
function getUser(id: UserId): Promise<User> {
  return fetch(`/api/users/${id}`).then(r => r.json());
}

function getOrder(id: OrderId): Promise<Order> {
  return fetch(`/api/orders/${id}`).then(r => r.json());
}

const userId = createUserId("usr_abc123");
const orderId = "ord_xyz789" as OrderId;

getUser(userId);   // OK
// getUser(orderId); // Błąd! OrderId nie jest UserId
// getUser("some-string"); // Błąd! string nie jest UserId

// Branded types zapobiegają mieszaniu walut
function addUSD(a: USD, b: USD): USD {
  return (a + b) as USD;
}

const price = createUSD(29.99);
const tax = createUSD(5.00);
addUSD(price, tax); // OK
// addUSD(price, 100 as EUR); // Błąd!

Discriminated Unions#

Discriminated unions (unie dyskryminowane) to wzorzec, w którym wspólne pole (dyskryminator) pozwala TypeScript zawęzić typ unii.

// Definiowanie discriminated union
interface LoadingState {
  status: "loading";
}

interface SuccessState<T> {
  status: "success";
  data: T;
}

interface ErrorState {
  status: "error";
  error: {
    message: string;
    code: number;
  };
}

type AsyncState<T> = LoadingState | SuccessState<T> | ErrorState;

// Exhaustive pattern matching
function renderState<T>(state: AsyncState<T>): string {
  switch (state.status) {
    case "loading":
      return "Loading...";
    case "success":
      return `Data: ${JSON.stringify(state.data)}`;
    case "error":
      return `Error ${state.error.code}: ${state.error.message}`;
    default:
      // Exhaustiveness check - nigdy nie powinien zostać osiągnięty
      const _exhaustive: never = state;
      return _exhaustive;
  }
}

// Bardziej złożony przykład - system płatności
type PaymentMethod =
  | { type: "card"; cardNumber: string; expiryDate: string; cvv: string }
  | { type: "bankTransfer"; iban: string; bic: string }
  | { type: "paypal"; email: string }
  | { type: "crypto"; walletAddress: string; network: "ethereum" | "bitcoin" };

function processPayment(method: PaymentMethod): void {
  switch (method.type) {
    case "card":
      console.log(`Processing card ending in ${method.cardNumber.slice(-4)}`);
      break;
    case "bankTransfer":
      console.log(`Transferring to IBAN: ${method.iban}`);
      break;
    case "paypal":
      console.log(`PayPal payment to ${method.email}`);
      break;
    case "crypto":
      console.log(`Crypto payment on ${method.network}: ${method.walletAddress}`);
      break;
  }
}

// Generyczny Result type (inspirowany Rust)
type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

function ok<T>(value: T): Result<T, never> {
  return { ok: true, value };
}

function err<E>(error: E): Result<never, E> {
  return { ok: false, error };
}

function divide(a: number, b: number): Result<number, string> {
  if (b === 0) return err("Division by zero");
  return ok(a / b);
}

const result = divide(10, 2);
if (result.ok) {
  console.log(result.value); // typ: number
} else {
  console.log(result.error); // typ: string
}

Pattern Matching z Generics#

TypeScript nie ma wbudowanego pattern matchingu jak Rust czy Scala, ale możemy go zasymulować za pomocą generics i conditional types.

// Typ-level pattern matching
type Match<T, Cases extends [any, any][]> =
  Cases extends [[infer Pattern, infer Result], ...infer Rest extends [any, any][]]
    ? T extends Pattern
      ? Result
      : Match<T, Rest>
    : never;

// Praktyczny matcher na poziomie wartości
type Matcher<T, R> = {
  [K in T extends { type: infer U extends string } ? U : never]: (
    value: Extract<T, { type: K }>
  ) => R;
};

function match<T extends { type: string }, R>(
  value: T,
  handlers: Matcher<T, R>
): R {
  const handler = (handlers as any)[value.type];
  return handler(value);
}

// Użycie z discriminated union
type Shape =
  | { type: "circle"; radius: number }
  | { type: "rectangle"; width: number; height: number }
  | { type: "triangle"; base: number; height: number };

const area = match<Shape, number>(
  { type: "circle", radius: 5 },
  {
    circle: ({ radius }) => Math.PI * radius ** 2,
    rectangle: ({ width, height }) => width * height,
    triangle: ({ base, height }) => (base * height) / 2,
  }
);

Praktyczne wzorce projektowe#

Wzorzec Repository#

Generyczny wzorzec Repository abstrahuje operacje na danych, pozwalając pisac kod niezalezny od zrodla danych.

interface Entity {
  id: string;
  createdAt: Date;
  updatedAt: Date;
}

interface Repository<T extends Entity> {
  findById(id: string): Promise<T | null>;
  findAll(filter?: Partial<T>): Promise<T[]>;
  create(data: Omit<T, "id" | "createdAt" | "updatedAt">): Promise<T>;
  update(id: string, data: Partial<Omit<T, "id" | "createdAt">>): Promise<T>;
  delete(id: string): Promise<boolean>;
  count(filter?: Partial<T>): Promise<number>;
}

interface User extends Entity {
  name: string;
  email: string;
  role: "admin" | "user";
}

interface Product extends Entity {
  name: string;
  price: number;
  category: string;
}

class InMemoryRepository<T extends Entity> implements Repository<T> {
  private items: Map<string, T> = new Map();

  async findById(id: string): Promise<T | null> {
    return this.items.get(id) ?? null;
  }

  async findAll(filter?: Partial<T>): Promise<T[]> {
    let results = Array.from(this.items.values());
    if (filter) {
      results = results.filter((item) =>
        Object.entries(filter).every(
          ([key, value]) => item[key as keyof T] === value
        )
      );
    }
    return results;
  }

  async create(data: Omit<T, "id" | "createdAt" | "updatedAt">): Promise<T> {
    const now = new Date();
    const entity = {
      ...data,
      id: crypto.randomUUID(),
      createdAt: now,
      updatedAt: now,
    } as T;
    this.items.set(entity.id, entity);
    return entity;
  }

  async update(
    id: string,
    data: Partial<Omit<T, "id" | "createdAt">>
  ): Promise<T> {
    const existing = this.items.get(id);
    if (!existing) throw new Error(`Entity ${id} not found`);
    const updated = { ...existing, ...data, updatedAt: new Date() };
    this.items.set(id, updated);
    return updated;
  }

  async delete(id: string): Promise<boolean> {
    return this.items.delete(id);
  }

  async count(filter?: Partial<T>): Promise<number> {
    const items = await this.findAll(filter);
    return items.length;
  }
}

// Użycie
const userRepo: Repository<User> = new InMemoryRepository<User>();
const productRepo: Repository<Product> = new InMemoryRepository<Product>();

Wzorzec Builder#

Generyczny Builder pozwala budowac obiekty krok po kroku z pelnym wsparciem typow.

class Builder<T extends Record<string, any>> {
  private data: Partial<T> = {};

  set<K extends keyof T>(key: K, value: T[K]): this {
    this.data[key] = value;
    return this;
  }

  build(): T {
    return this.data as T;
  }
}

// Type-safe builder z required fields
type RequiredKeys<T, K extends keyof T> = Required<Pick<T, K>> &
  Partial<Omit<T, K>>;

class TypeSafeBuilder<T extends Record<string, any>, Set extends keyof T = never> {
  private data: Partial<T> = {};

  set<K extends keyof T>(
    key: K,
    value: T[K]
  ): TypeSafeBuilder<T, Set | K> {
    this.data[key] = value;
    return this as any;
  }

  build(this: TypeSafeBuilder<T, keyof T>): T {
    return this.data as T;
  }
}

interface Config {
  host: string;
  port: number;
  database: string;
  ssl: boolean;
}

const config = new TypeSafeBuilder<Config>()
  .set("host", "localhost")
  .set("port", 5432)
  .set("database", "mydb")
  .set("ssl", true)
  .build(); // OK - wszystkie pola ustawione

Wzorzec Factory#

Generyczna fabryka umozliwia tworzenie instancji roznych typow z wspolnym interfejsem.

// Generyczny interfejs Factory
interface Factory<T> {
  create(...args: any[]): T;
}

// Registry pattern z generics
class ServiceRegistry {
  private factories = new Map<string, Factory<any>>();

  register<T>(name: string, factory: Factory<T>): void {
    this.factories.set(name, factory);
  }

  resolve<T>(name: string): T {
    const factory = this.factories.get(name);
    if (!factory) {
      throw new Error(`Service "${name}" not registered`);
    }
    return factory.create();
  }
}

// Type-safe event emitter z generics
type EventMap = Record<string, any>;

class TypedEventEmitter<Events extends EventMap> {
  private handlers = new Map<keyof Events, Set<Function>>();

  on<K extends keyof Events>(
    event: K,
    handler: (payload: Events[K]) => void
  ): () => void {
    if (!this.handlers.has(event)) {
      this.handlers.set(event, new Set());
    }
    this.handlers.get(event)!.add(handler);

    // Zwróc funkcję do unsubscribe
    return () => {
      this.handlers.get(event)?.delete(handler);
    };
  }

  emit<K extends keyof Events>(event: K, payload: Events[K]): void {
    this.handlers.get(event)?.forEach((handler) => handler(payload));
  }
}

// Użycie
interface AppEvents {
  userLoggedIn: { userId: string; timestamp: Date };
  orderPlaced: { orderId: string; total: number };
  error: { message: string; code: number };
}

const emitter = new TypedEventEmitter<AppEvents>();

emitter.on("userLoggedIn", ({ userId, timestamp }) => {
  console.log(`User ${userId} logged in at ${timestamp}`);
});

emitter.on("orderPlaced", ({ orderId, total }) => {
  console.log(`Order ${orderId} placed: $${total}`);
});

emitter.emit("userLoggedIn", {
  userId: "usr_123",
  timestamp: new Date(),
});

// emitter.emit("userLoggedIn", { wrong: "data" }); // Błąd!

Zaawansowane techniki kombinowania typow#

Polaczenie wielu zaawansowanych technik pozwala tworzyc niezwykle ekspresyjne i bezpieczne API.

// Deep Partial - rekurencyjnie opcjonalne pola
type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};

// Deep Readonly - rekurencyjnie niemodyfikowalne
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

// Paths - generowanie sciezek do pol zagniezdzonego obiektu
type Paths<T, Prefix extends string = ""> = {
  [K in keyof T & string]: T[K] extends object
    ? Paths<T[K], `${Prefix}${K}.`> | `${Prefix}${K}`
    : `${Prefix}${K}`;
}[keyof T & string];

interface NestedConfig {
  database: {
    host: string;
    port: number;
    credentials: {
      user: string;
      password: string;
    };
  };
  cache: {
    ttl: number;
    enabled: boolean;
  };
}

type ConfigPaths = Paths<NestedConfig>;
// "database" | "database.host" | "database.port" | "database.credentials" | ...

// Type-safe deep get
function get<T, P extends string>(
  obj: T,
  path: P
): P extends `${infer K}.${infer Rest}`
  ? K extends keyof T
    ? Rest extends Paths<T[K]>
      ? any
      : never
    : never
  : P extends keyof T
    ? T[P]
    : never {
  return path.split(".").reduce((acc: any, key) => acc?.[key], obj);
}

Podsumowanie#

Generics w TypeScript to fundament tworzenia skalowalnego, bezpiecznego i wielokrotnie uzywalnego kodu. Kluczowe techniki, ktore omowilismy, obejmuja:

  • Funkcje, klasy i interfejsy generyczne - podstawa reużywalności kodu
  • Ograniczenia (extends) - precyzyjne kontrolowanie dozwolonych typów
  • Conditional types - logika na poziomie systemu typów
  • Mapped types - transformacja istniejących typów
  • Template literal types - operacje na typach stringowych
  • Infer - wyciąganie typów z wewnątrz innych typów
  • Utility types - gotowe narzędzia do manipulacji typami
  • Branded types - nominalne typowanie dla większego bezpieczeństwa
  • Discriminated unions - bezpieczne modelowanie stanów
  • Wzorce projektowe - Repository, Builder, Factory z generics

Opanowanie tych technik pozwala tworzyc systemy typow, ktore odzwierciedlaja logike biznesowa i wychwytuja bledy na etapie kompilacji, zanim trafią do produkcji.


W MDS Software Solutions Group TypeScript i zaawansowane generics to codziennosc naszego zespolu. Tworzymy typowo-bezpieczne aplikacje, ktore sa latwiejsze w utrzymaniu i rozwijaniu. Potrzebujesz wsparcia w projekcie TypeScript? Skontaktuj sie z nami - chetnie pomozemy!

Autor
MDS Software Solutions Group

Zespół ekspertów programistycznych specjalizujących się w nowoczesnych technologiach webowych.

TypeScript Generics - Zaawansowane typy w praktyce | MDS Software Solutions Group | MDS Software Solutions Group