TypeScript Generics - Zaawansowane typy w praktyce
TypeScript Generics Zaawansowane
technologieTypeScript 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!
Zespół ekspertów programistycznych specjalizujących się w nowoczesnych technologiach webowych.