Co się stanie po uruchomieniu tego kodu?
let x: number = 5;
let y: string = x;TypeScript nie pozwala na przypisanie wartości typu number do zmiennej typu string. To błąd kompilacji wykrywany przez system typów.
Wszystkie pytania z poprawnymi odpowiedziami i wyjaśnieniami.
let x: number = 5;
let y: string = x;TypeScript nie pozwala na przypisanie wartości typu number do zmiennej typu string. To błąd kompilacji wykrywany przez system typów.
? przy age?interface User {
name: string;
age?: number;
}
const user: User = { name: "Alice" };Znak ? przy polu interfejsu oznacza, że jest ono opcjonalne (optional). Obiekt nie musi go zawierać.
type Status = "active" | "inactive" | "pending";
const s: Status = "deleted";Union type ogranicza dozwolone wartości. 'deleted' nie jest częścią typu Status, więc kompilator zgłosi błąd.
identity<string>(42)?function identity<T>(arg: T): T {
return arg;
}
const result = identity<string>(42);Generyk T został jawnie ustawiony na string, ale argument 42 jest typu number. TypeScript zgłosi błąd kompilacji.
console.log(Direction.Up)?enum Direction {
Up,
Down,
Left,
Right
}
console.log(Direction.Up);W enum numerycznym bez jawnych wartości, pierwszy element ma wartość 0, kolejne rosną o 1.
push?const arr: readonly number[] = [1, 2, 3];
arr.push(4);Tablica readonly nie posiada metod mutujących jak push, pop, splice. Jest to zabezpieczenie na poziomie typów.
& w kontekście typów?type Point = {
x: number;
y: number;
};
type Point3D = Point & { z: number };
const p: Point3D = { x: 1, y: 2, z: 3 };Operator & tworzy intersection type, który łączy wszystkie właściwości z obu typów w jeden.
void?function greet(name: string): void {
console.log(`Hello, ${name}`);
}Typ void oznacza, że funkcja nie zwraca żadnej wartości. Różni się od never, który oznacza, że funkcja nigdy nie kończy działania.
as const w tym kontekście?const obj = { name: "Alice", age: 30 } as const;as const tworzy tzw. const assertion — wszystkie pola stają się readonly, a ich typy to literały (np. 'Alice' zamiast string).
type StringOrNumber = string | number;
function process(val: StringOrNumber) {
if (typeof val === "string") {
return val.toUpperCase();
}
return val.toFixed(2);
}Type narrowing to technika zawężania typu wewnątrz bloku warunkowego. TypeScript automatycznie rozpoznaje typ po sprawdzeniu typeof.
Config.interface Config {
debug: boolean;
}
interface Config {
verbose: boolean;
}
const cfg: Config = { debug: true, verbose: false };Declaration Merging to unikalna cecha interfejsów — wiele deklaracji o tej samej nazwie scala się w jeden typ. Type aliasy (type) tej możliwości nie mają.
function handleInput(val: unknown) {
return val.toUpperCase();
}Typ unknown to bezpieczna alternatywa dla any. Przed użyciem wartości musisz ją zawęzić np. przez typeof val === 'string'. To kluczowa różnica: any wyłącza sprawdzanie, unknown je wymusza.
val is string w typie zwracanym?function isString(val: unknown): val is string {
return typeof val === "string";
}
function process(input: unknown) {
if (isString(input)) {
console.log(input.toUpperCase());
}
}Type predicate (val is string) to custom type guard. Gdy funkcja zwróci true, TypeScript automatycznie zawęża typ w bloku warunkowym. Mocniejsze niż zwykły boolean.
let a: Object = "hello";
let b: object = "hello";
let c: {} = "hello";Object (duże O) i {} akceptują prymitywy (string, number). object (małe o) akceptuje tylko nie-prymitywne typy (obiekty, tablice, funkcje). String jest prymitywem, więc b zgłosi błąd.
new Animal()?abstract class Animal {
abstract speak(): string;
greet(): string {
return `I say: ${this.speak()}`;
}
}
class Dog extends Animal {
speak() { return "Woof!"; }
}
const a = new Animal();Klasa abstract to klasa bazowa bez możliwości tworzenia instancji. Wymusza implementację metod abstrakcyjnych w podklasach. new Dog() zadziała, new Animal() nie.
class User {
constructor(
public name: string,
private password: string,
protected role: string
) {}
}
const u = new User("Alice", "secret", "admin");
console.log(u.name, u.password, u.role);public — dostępny wszędzie. private — tylko wewnątrz klasy. protected — wewnątrz klasy i jej podklas. Z zewnątrz widoczny jest tylko name.
const user = {
name: "Alice",
address: {
city: null as string | null,
},
};
const city = user.address?.city ?? "Unknown";
console.log(city);?. (optional chaining) bezpiecznie dostaje się do zagnieżdżonych pól. ?? (nullish coalescing) zwraca prawą stronę tylko gdy lewa to null/undefined. city jest null, więc wynik to 'Unknown'.
! po input i czy jest bezpieczny?function getLength(input: string | null): number {
return input!.length;
}!. (non-null assertion) mówi kompilatorowi: 'wiem że to nie null'. Nie generuje żadnego kodu runtime — jeśli wartość faktycznie jest null, dostaniesz runtime error. Lepiej użyć type guard.
Pair i dlaczego wrong nie skompiluje się?type Pair = [string, number];
const p: Pair = ["age", 30];
const [label, value] = p;
const wrong: Pair = [30, "age"];Krotka (tuple) to tablica o ustalonej długości i konkretnych typach na każdej pozycji. [string, number] wymaga stringa na pozycji 0 i numbera na pozycji 1. Odwrócona kolejność to błąd.
type a interface w rozszerzaniu typów?interface User {
name: string;
}
type Admin = User & { role: string };
// vs
interface Manager extends User {
department: string;
}Interface rozszerza przez extends i wspiera declaration merging (wiele deklaracji). Type używa & (intersection) i obsługuje unie, krotki i prymitywy. Dla publicznych API rekomenduje się interface.
type Person = {
name: string
age: number
}
type Employee = {
name: string
email: string
}
type A = Person | Employee;
type B = Person & Employee;
const a: A = {
name: 'John',
age: 20,
email: 'john@test-email.co.uk',
}
const b: B = {
name: 'John',
age: 20,
email: 'john@test-email.co.uk',
}
console.log('A:', a.name, a.age, a.email)
console.log('B:', b.name, b.age, b.email)To pułapka! Mimo że obiekt a ma wszystkie pola, typ A = Person | Employee oznacza 'albo Person, albo Employee'. TypeScript nie wie, który wariant masz — może to być Person (bez email) lub Employee (bez age). Dlatego a.age i a.email zgłaszają błąd bez narrowingu. Natomiast B = Person & Employee wymaga WSZYSTKICH pól obu typów, więc b.name, b.age i b.email są gwarantowane.
val w ostatnim return?function formatValue(val: string | number | boolean) {
if (typeof val === "string") {
return val.toUpperCase();
}
if (typeof val === "number") {
return val.toFixed(2);
}
// Co wie TypeScript o val tutaj?
return val;
}TypeScript progressywnie zawęża typ po każdym typie guard. Po typeof === 'string' eliminuje string, po typeof === 'number' eliminuje number. Zostaje jedynie boolean. To automatyczny type narrowing — kompilator sam to oblicza.
res.data w bloku success i res.message w bloku error?type ApiResponse<T> =
| { status: "success"; data: T; timestamp: number }
| { status: "error"; message: string; code: number }
| { status: "loading" };
function handleResponse(res: ApiResponse<string[]>) {
if (res.status === "success") {
console.log(res.data.join(", "));
} else if (res.status === "error") {
console.log(res.message);
}
}status jako tag pozwala TypeScript zawęzić typ w każdej gałęziDiscriminated union (unia z dyskryminantem) to wzorzec, gdzie wspólne pole (status) pozwala TypeScript automatycznie zawęzić typ. W bloku status === 'success' kompilator wie, że masz wariant z data i timestamp. W error — message i code. Nie trzeba żadnych castów.
ConfigKey i ConfigValue?const config = {
apiUrl: "https://api.example.com",
timeout: 5000,
debug: false,
} as const;
type Config = typeof config;
type ConfigKey = keyof Config;
type ConfigValue = Config[ConfigKey];Dzięki as const wartości obiektu zachowują typy literalne (nie string/number/boolean). typeof config wyciąga typ obiektu. keyof daje unię kluczy. Config[ConfigKey] to indexed access type — unia WSZYSTKICH typów wartości. Potężna kombinacja do type-safe konfiguracji.
in jako type guard?interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
function move(animal: Bird | Fish) {
if ("fly" in animal) {
animal.fly();
} else {
animal.swim();
}
}if TypeScript wie, że mamy Bird'fly' in animal sprawdza w runtime, czy obiekt ma metodę fly. TypeScript to rozpoznaje jako type guard: w bloku if wie, że mamy Bird (bo tylko Bird ma fly). W else wie, że mamy Fish. Operator in to alternatywa dla instanceof gdy pracujesz z interfejsami.
type IsString<T> = T extends string ? "yes" : "no";
type A = IsString<string>; // ?
type B = IsString<number>; // ?Conditional types działają jak ternary: T extends string sprawdza, czy T rozszerza string. Dla string → 'yes', dla number → 'no'.
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? DeepReadonly<T[K]>
: T[K];
};DeepReadonly rekurencyjnie przechodzi przez wszystkie pola obiektu. Jeśli pole jest obiektem, stosuje się ponownie. Wynik: cały obiekt jest niemutowalny.
Result?type ExtractReturn<T> = T extends (...args: any[]) => infer R
? R
: never;
type Result = ExtractReturn<() => Promise<string>>;Słowo kluczowe infer R wyciąga typ zwracany funkcji. Funkcja zwraca Promise<string>, więc R = Promise<string>.
ClickEvent?type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<"click">;Template literal types pozwalają tworzyć nowe typy stringowe. Capitalize zamienia pierwszą literę na wielką: 'click' → 'Click', więc wynik to 'onClick'.
type Flatten<T> = T extends Array<infer U> ? U : T;
type A = Flatten<string[]>;
type B = Flatten<number>;Flatten wyciąga typ elementu z tablicy za pomocą infer. string[] → string. Dla number (nie-tablica) zwraca sam typ.
Result?type Exclude<T, U> = T extends U ? never : T;
type Result = Exclude<"a" | "b" | "c", "a" | "c">;Exclude działa dystrybucyjnie na union types. Dla każdego elementu: jeśli rozszerza U → never (usunięty), w przeciwnym razie zostaje. Zostaje tylko 'b'.
UserGetters?type Getters<T> = {
[K in keyof T as `get${Capitalize<K & string>}`]: () => T[K];
};
type UserGetters = Getters<{ name: string; age: number }>;Key remapping (as) w mapped types pozwala zmieniać nazwy kluczy. Tutaj każdy klucz K jest transformowany na get${Capitalize<K>}, a wartość staje się getterem.
assertNever?function assertNever(value: never): never {
throw new Error(`Unexpected value: ${value}`);
}
type Shape = "circle" | "square";
function area(shape: Shape) {
switch (shape) {
case "circle": return Math.PI;
case "square": return 1;
default: return assertNever(shape);
}
}assertNever przyjmuje never — typ, który nie powinien istnieć. Jeśli dodasz nowy wariant do Shape i nie obsłużysz go w switch, kompilator zgłosi błąd.
Result?type UnionToIntersection<U> =
(U extends any ? (x: U) => void : never) extends
(x: infer I) => void ? I : never;
type Result = UnionToIntersection<{ a: 1 } | { b: 2 }>;UnionToIntersection zamienia unię na intersekcję wykorzystując kontrawariantność pozycji parametru funkcji. Wynik: { a: 1 } & { b: 2 }.
payUSD(euros)?declare const brand: unique symbol;
type Branded<T, B> = T & { [brand]: B };
type USD = Branded<number, "USD">;
type EUR = Branded<number, "EUR">;
function payUSD(amount: USD) { /* ... */ }
const euros = 100 as EUR;
payUSD(euros);Branded types (typy nominalne) dodają 'phantom brand' do strukturalnego typu. USD i EUR są różnymi typami mimo że oba bazują na number.
-? i -readonly w mapped types?type MakeRequired<T> = {
[P in keyof T]-?: T[P];
};
type MakeMutable<T> = {
-readonly [P in keyof T]: T[P];
};
interface Config {
readonly host?: string;
readonly port?: number;
}
type WritableConfig = MakeMutable<MakeRequired<Config>>;Prefix - w mapped types usuwa modyfikator: -? czyni pole wymaganym (jak Required<T>), -readonly czyni pole mutowalnym. WritableConfig to { host: string; port: number }.
UserPreview i UserWithoutEmail?interface User {
id: number;
name: string;
email: string;
age: number;
}
type UserPreview = Pick<User, "id" | "name">;
type UserWithoutEmail = Omit<User, "email">;Pick<T, K> wybiera podane klucze z typu. Omit<T, K> usuwa podane klucze. Oba tworzą nowy typ z podzbiorem właściwości oryginału.
Record<Roles, boolean> i co by się stało, gdybyśmy pominęli klucz viewer?type Roles = "admin" | "editor" | "viewer";
type Permissions = Record<Roles, boolean>;
const perms: Permissions = {
admin: true,
editor: true,
viewer: false,
};Record<K, V> tworzy typ z kluczami K i wartościami V. Wszystkie klucze są wymagane — pominięcie któregokolwiek to błąd kompilacji.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rect"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rect":
return shape.width * shape.height;
case "triangle":
return (shape.base * shape.height) / 2;
}
}kind pozwala TypeScript zawęzić typ w każdej gałęziDiscriminated Union to unie z polem-tagiem (discriminant), np. kind. TypeScript automatycznie zawęża typ po sprawdzeniu taga w switch/if — daje dostęp do pól specyficznych dla wariantu.
function logLength<T extends { length: number }>(obj: T): void {
console.log(obj.length);
}
logLength("hello");
logLength([1, 2, 3]);
logLength(42);extends { length: number } tego wymagaGeneric constraint T extends { length: number } wymaga, żeby T miał pole length. String i tablica mają length, więc przechodzą. Number nie ma length — błąd kompilacji.
Result?function getParams<T extends (...args: any[]) => any>(
fn: T
): Parameters<T> {
return [] as any;
}
type Result = Parameters<(a: string, b: number) => void>;Parameters<T> wyciąga typy parametrów funkcji jako krotkę (tuple). Dla (a: string, b: number) => void zwraca [string, number].
Result?type NonStringKeys<T> = {
[K in keyof T]: T[K] extends string ? never : K;
}[keyof T];
interface User {
id: number;
name: string;
age: number;
email: string;
}
type Result = NonStringKeys<User>;Ten typ filtruje klucze: jeśli wartość to string → never (usunięty), w przeciwnym razie zachowuje nazwę klucza. id i age mają typ number, więc zostają. name i email (string) są usunięte.
Numbers?type Extract<T, U> = T extends U ? T : never;
type Numbers = Extract<string | number | boolean, number | boolean>;Extract<T, U> jest odwrotnością Exclude — zachowuje z unii T tylko typy przypisywalne do U. string nie jest w U, więc jest usunięty. Zostaje number | boolean.
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true
}
}
function add(a, b) {
return a + b;
}strict: true włącza m.in. noImplicitAny. Parametry a i b nie mają typów i nie da się ich wydedukować z kontekstu — kompilator domyślnie przypisałby any, ale flaga to blokuje.
type ReadOnly<T> = { readonly [P in keyof T]: T[P] };
type MyPartial<T> = { [P in keyof T]?: T[P] };
interface Todo {
title: string;
done: boolean;
}
type A = ReadOnly<Todo>;
type B = MyPartial<Todo>;Mapped types iterują po kluczach typu i pozwalają dodawać modyfikatory. ReadOnly dodaje readonly do każdego pola, MyPartial dodaje ? (opcjonalność). Tak działają wbudowane Readonly<T> i Partial<T>.
CSSSpacing i jak się je generuje?type CSSProperty = "margin" | "padding";
type Direction = "top" | "right" | "bottom" | "left";
type CSSSpacing = {
[K in `${CSSProperty}-${Direction}`]: string;
};
const spacing: CSSSpacing = {
"margin-top": "10px",
"margin-right": "20px",
// ... wymaga wszystkich 8 kombinacji
};Template literal type w mapped type tworzy WSZYSTKIE kombinacje: margin-top, margin-right, margin-bottom, margin-left, padding-top, padding-right, padding-bottom, padding-left = 8 pól. TypeScript generuje kartezjański iloczyn stringów. Potężne narzędzie do tworzenia typów CSS-in-JS.
type Awaited<T> =
T extends Promise<infer U>
? Awaited<U>
: T;
type A = Awaited<Promise<string>>;
type B = Awaited<Promise<Promise<number>>>;
type C = Awaited<boolean>;Awaited rekurencyjnie 'rozpakowuje' Promise: Promise<string> → string, Promise<Promise<number>> → Promise<number> → number. Jeśli T nie jest Promise, zwraca T bez zmian (boolean). To uproszczona wersja wbudowanego typu Awaited<T> z TypeScript 4.5+.
type ToArray<T> = T extends any ? T[] : never;
type A = ToArray<string | number>;
type B = ToArray<string | number | boolean>;Conditional types z parametrem T są dystrybucyjne nad uniami! TypeScript stosuje warunek do KAŻDEGO członka unii osobno: string → string[], number → number[]. Wynik to unia tablic, nie tablica unii. Żeby wyłączyć dystrybucję, opakuj T w [T].
r i g i czym satisfies różni się od : Colors?type Colors = Record<string, [number, number, number] | string>;
const palette = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0, 255],
} satisfies Colors;
const r = palette.red; // jaki typ?
const g = palette.green; // jaki typ?Operator satisfies (TS 4.9+) sprawdza, czy wartość jest zgodna z typem, ale NIE rozszerza typu zmiennej. Z : Colors oba pola miałyby typ [number,number,number] | string. Z satisfies TypeScript zachowuje wąskie typy — red to krotka, green to string.
StringFields i NumberFields?type PickByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
interface Model {
id: number;
name: string;
active: boolean;
email: string;
age: number;
}
type StringFields = PickByType<Model, string>;
type NumberFields = PickByType<Model, number>;Key remapping z as + conditional type pozwala filtrować klucze. Gdy T[K] extends U jest fałszywe, klucz mapuje się na never — co go usuwa z wyniku. Wynik: StringFields ma pola, których wartości to string, NumberFields — number.
console.log i jak nazywa się ta technika?const add = (a: number) => (b: number) => a + b;
const add5 = add(5);
console.log(add5(3));Currying to zamiana funkcji wielu argumentów na sekwencję funkcji jednoargumentowych. add(5) zwraca (b) => 5 + b, więc add5(3) = 8.
transform(5)?const pipe = <T>(...fns: Array<(arg: T) => T>) =>
(value: T): T =>
fns.reduce((acc, fn) => fn(acc), value);
const transform = pipe(
(x: number) => x * 2,
(x: number) => x + 1,
(x: number) => x * 3
);
console.log(transform(5));Pipe wykonuje funkcje od lewej do prawej: 5 → *2 = 10 → +1 = 11 → *3 = 33.
type Option<T> = { tag: "some"; value: T } | { tag: "none" };
const map = <A, B>(opt: Option<A>, fn: (a: A) => B): Option<B> =>
opt.tag === "some"
? { tag: "some", value: fn(opt.value) }
: { tag: "none" };
const result = map({ tag: "some", value: 5 }, x => x * 2);Option (Maybe) monad opakowuje wartość, która może nie istnieć. Funkcja map to operacja funktorowa — transformuje wartość wewnątrz kontenera bez zmiany struktury.
const compose = <A, B, C>(
f: (b: B) => C,
g: (a: A) => B
) => (a: A): C => f(g(a));
const double = (x: number) => x * 2;
const toString = (x: number) => `Value: ${x}`;
const doubleThenString = compose(toString, double);
console.log(doubleThenString(5));Compose wykonuje funkcje od prawej do lewej (matematyczna kompozycja f∘g). Pipe robi odwrotnie — od lewej do prawej.
flatMap w kontekście monad?type Result<T, E> =
| { ok: true; value: T }
| { ok: false; error: E };
const flatMap = <T, E, U>(
result: Result<T, E>,
fn: (value: T) => Result<U, E>
): Result<U, E> =>
result.ok ? fn(result.value) : result;
const parse = (s: string): Result<number, string> => {
const n = Number(s);
return isNaN(n)
? { ok: false, error: "Not a number" }
: { ok: true, value: n };
};flatMap (bind/chain) to fundamentalna operacja monadyczna. Różni się od map tym, że funkcja zwraca nowy kontener (Result), a flatMap 'spłaszcza' wynik.
const memoize = <T extends (...args: any[]) => any>(fn: T): T => {
const cache = new Map<string, ReturnType<T>>();
return ((...args: Parameters<T>): ReturnType<T> => {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key)!;
const result = fn(...args);
cache.set(key, result);
return result;
}) as T;
};
const factorial = memoize((n: number): number =>
n <= 1 ? 1 : n * factorial(n - 1)
);Memoizacja to technika zapamiętywania wyników funkcji dla danych argumentów. Kolejne wywołanie z tymi samymi argumentami zwraca wynik z cache zamiast obliczać ponownie.
updated?type Lens<S, A> = {
get: (s: S) => A;
set: (a: A, s: S) => S;
};
const nameLens: Lens<{ name: string; age: number }, string> = {
get: (s) => s.name,
set: (a, s) => ({ ...s, name: a }),
};
const user = { name: "Alice", age: 30 };
const updated = nameLens.set("Bob", user);Lens to wzorzec z programowania funkcyjnego do immutable aktualizacji zagnieżdżonych struktur. Set tworzy nowy obiekt z zaktualizowanym polem, zachowując resztę.
type Predicate<T> = (value: T) => boolean;
const and = <T>(...preds: Predicate<T>[]): Predicate<T> =>
(value) => preds.every(p => p(value));
const or = <T>(...preds: Predicate<T>[]): Predicate<T> =>
(value) => preds.some(p => p(value));
const not = <T>(pred: Predicate<T>): Predicate<T> =>
(value) => !pred(value);
const isEven: Predicate<number> = n => n % 2 === 0;
const isPositive: Predicate<number> = n => n > 0;
const isOddAndPositive = and(not(isEven), isPositive);
console.log(isOddAndPositive(3), isOddAndPositive(-3));Kompozycja predykatów: 3 jest nieparzyste (not even) i pozytywne → true. -3 jest nieparzyste ale nie pozytywne → false.
Task?type Task<T> = {
fork: (reject: (e: Error) => void, resolve: (t: T) => void) => void;
};
const taskOf = <T>(value: T): Task<T> => ({
fork: (_, resolve) => resolve(value),
});
const taskMap = <A, B>(task: Task<A>, fn: (a: A) => B): Task<B> => ({
fork: (reject, resolve) =>
task.fork(reject, (a) => resolve(fn(a))),
});Task to leniwa monada asynchroniczna. W przeciwieństwie do Promise, nie wykonuje się od razu — obliczenie startuje dopiero przy wywołaniu fork().
const trampoline = <T>(fn: () => T | (() => T)): T => {
let result = fn();
while (typeof result === "function") {
result = (result as () => T)();
}
return result;
};
const sumTo = (n: number, acc: number = 0): any =>
n === 0 ? acc : () => sumTo(n - 1, acc + n);
console.log(trampoline(() => sumTo(100000)));Trampoline zamienia rekursję ogonową na pętlę while, unikając stack overflow. Zamiast wywoływać się rekurencyjnie, zwraca thunk (funkcję), który jest wykonywany w pętli.
const double = (x: number): number => x * 2;
const result1 = double(5);
const result2 = double(5);
let counter = 0;
const impureDouble = (x: number): number => {
counter++;
return x * 2;
};Pure function spełnia dwa warunki: (1) ten sam input → ten sam output, (2) zero side-effectów. impureDouble mutuje zewnętrzną zmienną counter, więc jest impure. Referential transparency oznacza, że double(5) zawsze można zastąpić wartością 10.
withLogging i jak się nazywa ten rodzaj funkcji?const withLogging = <T extends (...args: any[]) => any>(fn: T) =>
(...args: Parameters<T>): ReturnType<T> => {
console.log("Calling with:", args);
const result = fn(...args);
console.log("Result:", result);
return result;
};
const add = (a: number, b: number) => a + b;
const loggedAdd = withLogging(add);
loggedAdd(2, 3);Higher-Order Function (HOF) to funkcja, która przyjmuje lub zwraca inną funkcję. withLogging przyjmuje funkcję fn i zwraca nową funkcję z dodanym logowaniem — jak dekorator. W FP funkcje to first-class citizens.
function createCounter() {
let count = 0;
return {
increment: () => ++count,
getCount: () => count,
};
}
const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.getCount());Closure to mechanizm, w którym funkcja zachowuje dostęp do zmiennych z zakresu (scope), w którym została zdefiniowana — nawet po zakończeniu tego zakresu. count żyje w closure createCounter.
result?type Either<L, R> =
| { tag: "left"; value: L }
| { tag: "right"; value: R };
const left = <L>(value: L): Either<L, never> =>
({ tag: "left", value });
const right = <R>(value: R): Either<never, R> =>
({ tag: "right", value });
const map = <L, A, B>(
either: Either<L, A>,
fn: (a: A) => B
): Either<L, B> =>
either.tag === "right"
? right(fn(either.value))
: either;
const result = map(right(10), x => x * 2);Either to monada z dwoma wariantami: Left (konwencjonalnie błąd) i Right (sukces). Funkcja map transformuje wartość tylko w Right, Left przepuszcza bez zmian — alternatywa dla try/catch.
type Thunk<T> = () => T;
const lazyValue: Thunk<number> = () => {
console.log("Computing...");
return 42;
};
// Wartość nie jest jeszcze obliczona
console.log("Before");
const result = lazyValue();
console.log("After:", result);Thunk to opakowanie efektu/obliczenia w funkcję do późniejszego wywołania (lazy evaluation). Obliczenie nie startuje, dopóki nie wywołasz thunka — daje kontrolę nad momentem wykonania.
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Podejście imperatywne
let sum = 0;
for (let i = 0; i < numbers.length; i++) {
if (numbers[i] % 2 === 0) {
sum += numbers[i] * 2;
}
}
// Podejście deklaratywne
const result = numbers
.filter(n => n % 2 === 0)
.map(n => n * 2)
.reduce((acc, n) => acc + n, 0);Styl deklaratywny (FP) mówi 'co' zrobić: odfiltruj parzyste, podwój, zsumuj. Imperatywny mówi 'jak': iteruj, sprawdź, dodaj. Deklaratywny jest bardziej czytelny i mniej podatny na błędy.
safeDivide (partial) a totalDivide (total)?const safeDivide = (a: number, b: number): number | never => {
if (b === 0) throw new Error("Division by zero");
return a / b;
};
const totalDivide = (a: number, b: number): number =>
b === 0 ? 0 : a / b;Total function jest zdefiniowana dla każdego wejścia w swojej domenie — zawsze zwraca wynik. Partial function może rzucić wyjątek (jak safeDivide dla b=0). W FP preferujemy total functions.
const idempotent = (arr: number[]): number[] =>
[...new Set(arr)].sort((a, b) => a - b);
console.log(idempotent([3, 1, 2, 1, 3]));
console.log(idempotent(idempotent([3, 1, 2, 1, 3])));Idempotentność: f(f(x)) === f(x). Pierwsze wywołanie deduplikuje i sortuje tablicę. Drugie wywołanie na już przetworzonym wyniku da identyczny rezultat. Przykłady: Math.abs, Array.sort na posortowanej tablicy.
result? Które metody tablicowe są kluczowe w FP i dlaczego?const users = [
{ name: "Alice", age: 30 },
{ name: "Bob", age: 25 },
{ name: "Charlie", age: 35 },
];
const result = users
.filter(u => u.age >= 30)
.map(u => u.name)
.reduce((acc, name) => acc + ", " + name);filter (wybierz), map (transformuj) i reduce (złóż) to fundamentalne operacje FP na kolekcjach. Są deklaratywne, nie mutują źródła i można je łączyć w łańcuch (pipeline). Wynik: Alice i Charlie mają >= 30 lat.
result i fallback?const Maybe = <T>(value: T | null | undefined) => ({
map: <U>(fn: (val: T) => U) =>
value != null ? Maybe(fn(value)) : Maybe<U>(null),
getOrElse: (defaultVal: T) =>
value != null ? value : defaultVal,
});
const result = Maybe("hello")
.map(s => s.toUpperCase())
.map(s => s + "!")
.getOrElse("default");
const fallback = Maybe<string>(null)
.map(s => s.toUpperCase())
.getOrElse("default");Maybe obsługuje null/undefined: jeśli wartość istnieje, map transformuje ją dalej. Jeśli jest null — cały łańcuch jest pomijany, a getOrElse zwraca wartość domyślną. Alternatywa dla łańcuchów if-null.
result i dlaczego ten styl kodu jest preferowany w FP?const users = [
{ name: "Alice", age: 30, role: "admin" },
{ name: "Bob", age: 25, role: "user" },
{ name: "Charlie", age: 35, role: "admin" },
{ name: "Diana", age: 28, role: "user" },
] as const;
const result = users
.filter(u => u.role === "admin")
.map(u => ({ ...u, name: u.name.toUpperCase() }))
.sort((a, b) => a.age - b.age);Pipeline filter→map→sort to esencja FP: każda operacja tworzy nową strukturę, oryginał zostaje nietkniięty. Deklaratywny styl mówi CO robisz (filtruj adminów, zamień na uppercase, posortuj po wieku), nie JAK. Łatwy do czytania, testowania i debugowania.
type Fn<A, B> = (a: A) => B;
const pipe2 = <A, B, C>(
f: Fn<A, B>,
g: Fn<B, C>
): Fn<A, C> => (a) => g(f(a));
const parse = (s: string): number => parseInt(s, 10);
const double = (n: number): number => n * 2;
const format = (n: number): string => `Result: ${n}`;
const transform = pipe2(pipe2(parse, double), format);
console.log(transform("21"));pipe2 łączy dwie funkcje: f: A→B i g: B→C dając A→C. Generyki gwarantują, że typy się zgadzają — nie da się połączyć string→number z boolean→string, bo B musi pasować. parse('21')=21, double(21)=42, format(42)='Result: 42'.
Tree<T>?type Tree<T> =
| { type: "leaf"; value: T }
| { type: "node"; left: Tree<T>; right: Tree<T> };
const sumTree = (tree: Tree<number>): number =>
tree.type === "leaf"
? tree.value
: sumTree(tree.left) + sumTree(tree.right);
const myTree: Tree<number> = {
type: "node",
left: { type: "leaf", value: 1 },
right: {
type: "node",
left: { type: "leaf", value: 2 },
right: { type: "leaf", value: 3 },
},
};
console.log(sumTree(myTree));Tree<T> to algebraiczny typ danych (ADT) — discriminated union z rekurencyjną definicją. sumTree rekurencyjnie sumuje: node(leaf(1), node(leaf(2), leaf(3))) = 1 + (2 + 3) = 6. ADT to fundament FP — modelują struktury danych bez klas, czysto przez typy.
const map = <A, B>(arr: A[], fn: (a: A) => B): B[] =>
arr.map(fn);
const identity = <T>(x: T): T => x;
const double = (x: number) => x * 2;
const addOne = (x: number) => x + 1;
// Law 1: Identity
map([1, 2, 3], identity) // === [1, 2, 3]
// Law 2: Composition
map(map([1, 2, 3], double), addOne)
// ===
map([1, 2, 3], x => addOne(double(x)))Funktor to kontener (Array, Maybe, Promise) z operacją map spełniającą dwa prawa: (1) mapowanie identyczności nie zmienia kontenera, (2) mapowanie dwóch funkcji po kolei daje ten sam wynik co mapowanie ich kompozycji. Gwarantuje to przewidywalne zachowanie.
result i co to jest transducer?type Reducer<A, B> = (acc: B, val: A) => B;
type Transducer<A, B> = <C>(rf: Reducer<B, C>) => Reducer<A, C>;
const mapT = <A, B>(fn: (a: A) => B): Transducer<A, B> =>
(rf) => (acc, val) => rf(acc, fn(val));
const filterT = <A>(pred: (a: A) => boolean): Transducer<A, A> =>
(rf) => (acc, val) => pred(val) ? rf(acc, val) : acc;
const nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const isEven = (n: number) => n % 2 === 0;
const triple = (n: number) => n * 3;
const xform = (rf: Reducer<number, number[]>) =>
filterT(isEven)(mapT(triple)(rf));
const result = nums.reduce(
xform((acc, val) => [...acc, val]),
[] as number[]
);Transducer łączy filter i map w jednym przebiegu — nie tworzy pośrednich tablic jak chain .filter().map(). Filtruje parzyste (2,4,6,8,10) i mnoży ×3 w jednej redukcji. Wydajniejsze od chainów na dużych zbiorach danych.
interface ButtonProps {
label: string;
onClick: () => void;
variant?: "primary" | "secondary";
}
const Button = ({ label, onClick, variant = "primary" }: ButtonProps) => (
<button onClick={onClick} className={variant}>
{label}
</button>
);Najlepszą praktyką jest zdefiniowanie interface/type dla propsów i użycie go jako typu parametru. Opcjonalne propsy oznaczamy ?, a wartości domyślne podajemy w destrukturyzacji.
setCount('hello') spowoduje błąd?const [count, setCount] = useState(0);
setCount("hello");TypeScript inferencuje typ stanu z wartości początkowej. useState(0) daje typ number, więc setCount akceptuje tylko number. Dla unii typów użyj useState<string | number>(0).
useState<User | null>(null)?const [user, setUser] = useState<User | null>(null);
if (user) {
console.log(user.name);
}null — nie wiedziałby o UserGdy wartość początkowa to null, TypeScript inferencuje typ jako null. Jawny generyk <User | null> mówi: stan może być User albo null. Po sprawdzeniu if (user) TypeScript zawęża typ do User.
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log(e.currentTarget.value);
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};React dostarcza własne typy eventów (React.MouseEvent, React.ChangeEvent, React.FormEvent itp.) z generykiem na element HTML. Daje to pełny dostęp do typowanych właściwości elementu przez currentTarget.
useRef<HTMLInputElement>(null) potrzebuje generyka?const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} />;useRef<T>(null) tworzy ref z typem T | null. Generyk mówi TypeScript, że .current będzie HTMLInputElement (lub null przed mount). Dzięki temu masz pełny autocomplete na DOM API.
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
return <ul>{items.map((item, i) => <li key={i}>{renderItem(item)}</li>)}</ul>;
}
<List items={["a", "b"]} renderItem={(item) => <span>{item}</span>} />Generic component używa parametru typu <T> zarówno w interfejsie propsów jak i w komponencie. TypeScript inferencuje T z przekazanych danych — tu string[] → T = string. Renderowanie jest w pełni typowane.
ThemeContextType | undefined?interface ThemeContextType {
theme: "light" | "dark";
toggle: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error("useTheme must be used within ThemeProvider");
return ctx;
}createContext potrzebuje wartości domyślnej. Undefined + custom hook z guardem to bezpieczny wzorzec: jeśli komponent nie jest w providerze, hook rzuca czytelny błąd zamiast dawać undefined.
children i czym się różni od React.ReactElement?interface CardProps {
title: string;
children: React.ReactNode;
}
const Card = ({ title, children }: CardProps) => (
<div>
<h2>{title}</h2>
<div>{children}</div>
</div>
);React.ReactNode to najszerszy typ dla children: JSX, string, number, boolean, null, undefined, tablice. React.ReactElement to wyłącznie element JSX (<Component /> lub <div />). Dla children prawie zawsze użyj ReactNode.
React.ComponentPropsWithoutRef<'button'>?type ButtonProps = React.ComponentPropsWithoutRef<"button"> & {
variant?: "primary" | "danger";
};
const Button = ({ variant = "primary", children, ...rest }: ButtonProps) => (
<button className={variant} {...rest}>
{children}
</button>
);ComponentPropsWithoutRef<'element'> wyciąga pełny zestaw propsów natywnego elementu HTML. Połączenie z & pozwala dodać własne propsy (variant). Wzorzec idealny do tworzenia reużywalnych komponentów UI.
React.memo współpracuje z TypeScript?const MemoizedList = React.memo(function UserList({
users,
onSelect,
}: {
users: User[];
onSelect: (user: User) => void;
}) {
return (
<ul>
{users.map((u) => (
<li key={u.id} onClick={() => onSelect(u)}>{u.name}</li>
))}
</ul>
);
});React.memo() jest generyczny i automatycznie inferencuje typy propsów z opakowanego komponentu. TypeScript zapewnia, że porównywanie propsów (shallow comparison) jest type-safe.
dispatch z useReducer?type Action =
| { type: "increment" }
| { type: "decrement" }
| { type: "set"; payload: number };
function reducer(state: number, action: Action): number {
switch (action.type) {
case "increment": return state + 1;
case "decrement": return state - 1;
case "set": return action.payload;
}
}
const [count, dispatch] = useReducer(reducer, 0);useReducer inferencuje typ dispatch z typu Action. Discriminated union (pole type) gwarantuje, że dispatch przyjmie tylko poprawne akcje. action.payload jest dostępny tylko w case 'set'.
useFetch używa generyka <T,>?const useFetch = <T,>(url: string) => {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
fetch(url)
.then((res) => res.json())
.then((json: T) => setData(json))
.catch((err) => setError(err))
.finally(() => setLoading(false));
}, [url]);
return { data, loading, error };
};
const { data } = useFetch<User[]>("/api/users");W plikach .tsx <T> byłoby zinterpretowane jako JSX tag. <T,> to obejście — trailing comma odróżnia generyk od taga. Caller podaje useFetch<User[]>(url) i data ma typ User[] | null.
React.forwardRef?const ForwardedInput = React.forwardRef<
HTMLInputElement,
{ label: string }
>(({ label, ...props }, ref) => (
<div>
<label>{label}</label>
<input ref={ref} {...props} />
</div>
));React.forwardRef<RefType, PropsType> — pierwszym generykiem jest typ elementu DOM (ref), drugim typ propsów. Umożliwia to komponentowi-dziecku przekazywanie ref do wewnętrznego elementu.
React.ReactNode a React.ReactElement w propsach?type PropsWithChildren<P = {}> = P & {
children?: React.ReactNode;
};
interface LayoutProps {
sidebar: React.ReactElement;
}
const Layout = ({ sidebar, children }: PropsWithChildren<LayoutProps>) => (
<div>
<aside>{sidebar}</aside>
<main>{children}</main>
</div>
);Użyj ReactNode dla children (akceptuje wszystko). Użyj ReactElement gdy wymagasz konkretnego komponentu JSX (np. sidebar musi być elementem, nie tekstem). To ważna różnica w typowaniu layoutów.
AsyncState<T> jest lepszy od State<T> do zarządzania stanem asynchronicznym?type Status = "idle" | "loading" | "success" | "error";
interface State<T> {
status: Status;
data: T | null;
error: string | null;
}
type AsyncState<T> =
| { status: "idle"; data: null; error: null }
| { status: "loading"; data: null; error: null }
| { status: "success"; data: T; error: null }
| { status: "error"; data: null; error: string };Discriminated union na statusie eliminuje niemożliwe stany. TypeScript wie: success → data: T (nie null), error → error: string (nie null). State<T> pozwala na error + data jednocześnie — to bug waiting to happen.
useCallback i dlaczego as string jest potrzebne przy formData.get?const handleSubmit = useCallback(
async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const email = formData.get("email") as string;
await submitForm({ email });
},
[]
);FormData.get() zwraca string | File | null bo formularze mogą mieć pliki. Gdy wiesz że pole to tekst, as string zawęża typ. Alternatywnie: użyj type guard typeof val === 'string'.
| undefined w odróżnieniu od wcześniejszego przykładu?type Theme = "light" | "dark";
const ThemeContext = createContext<Theme>("light");
const useTheme = () => useContext(ThemeContext);
const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
const [theme, setTheme] = useState<Theme>("light");
return (
<ThemeContext.Provider value={theme}>
{children}
</ThemeContext.Provider>
);
};Gdy createContext dostaje sensowną wartość domyślną ("light"), komponent dostanie tę wartość nawet bez providera. Nie trzeba undefined ani guarda. Wzorzec z undefined jest potrzebny gdy NIE MA sensownej wartości domyślnej.
type ButtonVariant = "primary" | "secondary" | "danger";
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
isLoading?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ variant = "primary", isLoading, children, disabled, ...props }, ref) => (
<button
ref={ref}
disabled={disabled || isLoading}
className={variant}
{...props}
>
{isLoading ? "Loading..." : children}
</button>
)
);Wzorzec extends HTMLAttributes to standard w design systemach. Komponent akceptuje wszystkie natywne propsy <button> (onClick, disabled, type itp.) + własne (variant, isLoading). forwardRef umożliwia przekazywanie ref.
const useLocalStorage = <T,>(
key: string,
initialValue: T
): [T, (value: T | ((prev: T) => T)) => void] => {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? (JSON.parse(item) as T) : initialValue;
} catch {
return initialValue;
}
});
const setValue = (value: T | ((prev: T) => T)) => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
};
return [storedValue, setValue];
};Hook naśladuje API useState — zwraca krotkę [value, setter]. Typ unii T | ((prev: T) => T) pozwala na setter zarówno z wartością jak i z callbackiem. To ten sam wzorzec co wbudowany setState.
keyof T w typie Column<T>?type Column<T> = {
key: keyof T;
header: string;
render?: (value: T[keyof T], row: T) => React.ReactNode;
};
interface TableProps<T> {
data: T[];
columns: Column<T>[];
}
function Table<T extends Record<string, unknown>>({ data, columns }: TableProps<T>) {
return (
<table>
<thead>
<tr>{columns.map(col => <th key={String(col.key)}>{col.header}</th>)}</tr>
</thead>
<tbody>
{data.map((row, i) => (
<tr key={i}>
{columns.map(col => (
<td key={String(col.key)}>
{col.render ? col.render(row[col.key], row) : String(row[col.key])}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}key w kolumnie jest prawdziwym kluczem typu danych T — nie można podać nieistniejącego pola. TypeScript autocompletuje dostępne kluczekeyof T w generycznym komponencie Table gwarantuje type-safety: kolumny mogą tylko odwoływać się do istniejących pól danych. Dodanie nowego pola do danych automatycznie udostępnia go w kolumnach.
type PolymorphicProps<E extends React.ElementType> = {
as?: E;
children: React.ReactNode;
} & Omit<React.ComponentPropsWithoutRef<E>, "as" | "children">;
function Box<E extends React.ElementType = "div">({
as,
children,
...props
}: PolymorphicProps<E>) {
const Component = as || "div";
return <Component {...props}>{children}</Component>;
}
<Box as="a" href="/about">Link</Box>
<Box as="button" onClick={() => {}}>Click</Box>
<Box>Default div</Box>as do zmiany taga HTML — TypeScript automatycznie typuje propsy na podstawie wybranego elementuPolymorphic component (wzorzec popularny w Chakra UI, Radix) pozwala zmienić renderowany element przez prop as. TypeScript dzięki generykom wie: jeśli as='a', to propsy include href. Jeśli as='button' — propsy include onClick. Pełne type-safety.
{ data: T | null; loading: boolean; error: Error | null }?type QueryResult<T> =
| { status: "idle"; data: undefined; error: undefined }
| { status: "loading"; data: undefined; error: undefined }
| { status: "success"; data: T; error: undefined }
| { status: "error"; data: undefined; error: Error };
function useQuery<T>(url: string): QueryResult<T> {
// implementation
}
const result = useQuery<User[]>("/api/users");
if (result.status === "success") {
result.data.map(u => u.name); // TypeScript wie!
}Z prostym { data, loading, error } nic nie chroni przed niemożliwymi stanami jak loading: true, data: someData, error: someError. Discriminated union wyklucza je na poziomie typów. Po status === 'success' TypeScript gwarantuje, że data to T (nie undefined).
interface DataListProps<T> {
items: T[];
filterFn?: (item: T) => boolean;
sortFn?: (a: T, b: T) => number;
children: (item: T, index: number) => React.ReactNode;
}
function DataList<T>({
items,
filterFn,
sortFn,
children,
}: DataListProps<T>) {
let processed = [...items];
if (filterFn) processed = processed.filter(filterFn);
if (sortFn) processed = processed.sort(sortFn);
return <>{processed.map((item, i) => children(item, i))}</>;
}
<DataList
items={users}
filterFn={u => u.active}
sortFn={(a, b) => a.name.localeCompare(b.name)}
>
{(user) => <UserCard key={user.id} user={user} />}
</DataList>DataList to generyczny komponent z render prop pattern. TypeScript inferencuje T z items i propaguje go do filterFn, sortFn i children. Separator logiki (filtrowanie, sortowanie) od prezentacji (render prop). Czyste FP w React.
handleChange('name', 42) i handleChange('unknown', 'value') zgłaszają błąd?type FormFields = {
name: string;
email: string;
age: number;
};
const useForm = <T extends Record<string, unknown>>(initial: T) => {
const [values, setValues] = useState<T>(initial);
const handleChange = <K extends keyof T>(
field: K,
value: T[K]
) => {
setValues(prev => ({ ...prev, [field]: value }));
};
return { values, handleChange };
};
const { values, handleChange } = useForm<FormFields>({
name: "",
email: "",
age: 0,
});
handleChange("name", "Alice"); // OK
handleChange("age", 25); // OK
handleChange("name", 42); // Error!
handleChange("unknown", "value"); // Error!Generyk K extends keyof T w handleChange tworzy zależność: field: K ogranicza do istniejących kluczy, value: T[K] wymaga typu odpowiadającego danemu kluczowi. TypeScript sprawdza OBA jednocześnie — nie da się podać złego pola ani złego typu wartości.
interface DialogProps {
open: boolean;
onClose: () => void;
children: React.ReactNode;
}
interface DialogComposition {
Header: React.FC<{ children: React.ReactNode }>;
Body: React.FC<{ children: React.ReactNode }>;
Footer: React.FC<{ children: React.ReactNode }>;
}
const Dialog: React.FC<DialogProps> & DialogComposition = ({
open,
onClose,
children,
}) => {
if (!open) return null;
return <div className="dialog">{children}</div>;
};
Dialog.Header = ({ children }) => <header>{children}</header>;
Dialog.Body = ({ children }) => <main>{children}</main>;
Dialog.Footer = ({ children }) => <footer>{children}</footer>;Compound Component pattern (popularny w Radix, Headless UI) używa dot notation do kompozycji. Typ FC<DialogProps> & DialogComposition to intersection — TypeScript wie, że Dialog jest jednocześnie komponentem i ma pod-komponenty. Zapewnia autocomplete na Dialog.