TypeScript Zaawansowany - Pytania Rekrutacyjne i Kompletny Przewodnik 2025

Zaawansowany TypeScript to kluczowy temat na rozmowach dla senior frontend/fullstack developerów. Znajomość generics, conditional types i mapped types odróżnia juniorów od seniorów. Rekruterzy sprawdzają nie tylko czy potrafisz czytać skomplikowane typy, ale czy umiesz je tworzyć i rozumiesz dlaczego są potrzebne.

Odpowiedź w 30 sekund

Co to są generics i conditional types?

Generics to parametry typów - pozwalają pisać reużywalny kod który zachowuje type safety dla różnych typów danych. function identity<T>(arg: T): T działa z każdym typem. Conditional types to typy warunkowe: T extends U ? X : Y - jeśli T spełnia warunek, wynikiem jest X, inaczej Y. Razem z infer i mapped types tworzą potężny system typów.

Odpowiedź w 2 minuty

TypeScript ma jeden z najbardziej zaawansowanych systemów typów wśród popularnych języków. Na poziomie senior oczekuje się znajomości czterech kluczowych mechanizmów:

Generics pozwalają tworzyć funkcje, klasy i typy które działają z różnymi typami danych. Zamiast pisać numberArray.filter() i stringArray.filter() osobno, definiujesz filter<T>(arr: T[], predicate: (item: T) => boolean): T[]. TypeScript inferuje T z argumentów lub możesz podać go explicite.

Conditional types to system "if-else" na poziomie typów. Składnia T extends U ? X : Y sprawdza czy T jest przypisywalny do U. Są fundamentem utility types jak Exclude<T, U>, Extract<T, U>, NonNullable<T>. Z infer możesz "wyciągać" typy z innych typów - np. ReturnType<T> wyciąga typ zwracany z funkcji.

Mapped types transformują właściwości istniejącego typu. { [K in keyof T]: ... } iteruje po kluczach T. Tak działają Partial<T>, Required<T>, Readonly<T>. Możesz modyfikować klucze, dodawać/usuwać modyfikatory readonly i optional.

Template literal types pozwalają tworzyć typy stringów przez konkatenację: `${Prefix}${Suffix}`. Używane do typowania event handlerów (onClick, onHover), CSS-in-JS, i API routes.

// Generics
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

// Conditional types
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>;      // false

// Mapped types
type Optional<T> = { [K in keyof T]?: T[K] };

// Template literal types
type EventName = `on${Capitalize<'click' | 'hover'>}`; // "onClick" | "onHover"

Generics - Podstawy

Czym są generics i po co ich używać?

Generics to parametry typów - placeholdery które są zastępowane konkretnymi typami przy użyciu. Pozwalają pisać reużywalny kod zachowując type safety.

// Bez generics - duplikacja kodu
function identityNumber(arg: number): number {
  return arg;
}
function identityString(arg: string): string {
  return arg;
}

// Lub utrata type safety
function identityAny(arg: any): any {
  return arg;
}

// Z generics - reużywalne i type-safe
function identity<T>(arg: T): T {
  return arg;
}

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

// Lub explicit
const explicit = identity<boolean>(true); // explicit: boolean

Jak działają constraints (ograniczenia) w generics?

extends ogranicza jakie typy mogą być użyte jako argument generyczny:

// T musi mieć właściwość length
function logLength<T extends { length: number }>(arg: T): void {
  console.log(arg.length);
}

logLength("hello");     // ✅ string ma length
logLength([1, 2, 3]);   // ✅ array ma length
logLength({ length: 5 }); // ✅ obiekt z length
// logLength(42);       // ❌ Error: number nie ma length

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

const person = { name: "John", age: 30 };
getProperty(person, "name"); // ✅ "John"
// getProperty(person, "email"); // ❌ Error: "email" nie jest kluczem

// Ograniczenie do typów prymitywnych
function compare<T extends string | number>(a: T, b: T): boolean {
  return a === b;
}

Jak używać wielu parametrów generycznych?

// Dwa parametry
function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}

const result = pair("hello", 42); // [string, number]

// Z constraints
function merge<T extends object, U extends object>(a: T, b: U): T & U {
  return { ...a, ...b };
}

const merged = merge({ name: "John" }, { age: 30 });
// merged: { name: string } & { age: number }

// Map z różnymi typami key/value
class TypedMap<K, V> {
  private items = new Map<K, V>();

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

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

const userMap = new TypedMap<number, string>();
userMap.set(1, "John");
// userMap.set("1", "John"); // ❌ Error: string nie jest number

Jak działają default type parameters?

// Domyślny typ gdy nie podano explicit
function createArray<T = string>(length: number, value: T): T[] {
  return Array(length).fill(value);
}

const strings = createArray(3, "x");  // string[] (inferred)
const numbers = createArray<number>(3, 0); // number[] (explicit)
const defaults = createArray(3, "x"); // string[] (default)

// W interfejsach
interface ApiResponse<T = unknown> {
  data: T;
  status: number;
  message: string;
}

const response: ApiResponse = { data: {}, status: 200, message: "OK" };
// data: unknown

const typedResponse: ApiResponse<User[]> = {
  data: [{ id: 1, name: "John" }],
  status: 200,
  message: "OK"
};
// data: User[]

Generics w klasach i interfejsach

// Generic interface
interface Repository<T> {
  find(id: number): T | undefined;
  findAll(): T[];
  save(entity: T): void;
  delete(id: number): void;
}

// Generic class implementująca interface
class InMemoryRepository<T extends { id: number }> implements Repository<T> {
  private items: T[] = [];

  find(id: number): T | undefined {
    return this.items.find(item => item.id === id);
  }

  findAll(): T[] {
    return [...this.items];
  }

  save(entity: T): void {
    const index = this.items.findIndex(item => item.id === entity.id);
    if (index >= 0) {
      this.items[index] = entity;
    } else {
      this.items.push(entity);
    }
  }

  delete(id: number): void {
    this.items = this.items.filter(item => item.id !== id);
  }
}

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

const userRepo = new InMemoryRepository<User>();
userRepo.save({ id: 1, name: "John" });
const user = userRepo.find(1); // User | undefined

Conditional Types

Jak działają conditional types?

Conditional types mają składnię T extends U ? X : Y - jeśli T jest przypisywalny do U, wynikiem jest X, inaczej Y:

// Podstawowy przykład
type IsString<T> = T extends string ? true : false;

type A = IsString<"hello">;  // true
type B = IsString<42>;       // false
type C = IsString<string>;   // true

// Praktyczny przykład - różne typy odpowiedzi
type ApiResponse<T> = T extends 'user'
  ? { id: number; name: string }
  : T extends 'product'
    ? { id: number; price: number }
    : never;

type UserResponse = ApiResponse<'user'>;     // { id: number; name: string }
type ProductResponse = ApiResponse<'product'>; // { id: number; price: number }
type Unknown = ApiResponse<'other'>;          // never

// Nested conditional
type TypeName<T> =
  T extends string ? "string" :
  T extends number ? "number" :
  T extends boolean ? "boolean" :
  T extends undefined ? "undefined" :
  T extends Function ? "function" :
  "object";

type T1 = TypeName<string>;    // "string"
type T2 = TypeName<() => void>; // "function"
type T3 = TypeName<string[]>;  // "object"

Co to jest distributive conditional type?

Gdy conditional type działa na union type, TypeScript "dystrybuuje" warunek na każdy element unii:

type ToArray<T> = T extends any ? T[] : never;

// Distributive - działa na każdy element osobno
type Result = ToArray<string | number>;
// = ToArray<string> | ToArray<number>
// = string[] | number[]

// NIE (string | number)[] - to byłoby (string | number)[]

// Wyłączenie distributive behavior
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;

type Result2 = ToArrayNonDist<string | number>;
// = (string | number)[]

// Praktyczne zastosowanie: Exclude i Extract
type Exclude<T, U> = T extends U ? never : T;
type Extract<T, U> = T extends U ? T : never;

type Numbers = Exclude<string | number | boolean, string>;
// = number | boolean (usunięto string)

type Strings = Extract<string | number | boolean, string>;
// = string (zostawiono tylko string)

Jak działa słowo kluczowe infer?

infer pozwala "wyciągnąć" typ z innego typu w conditional type:

// Wyciągnij typ zwracany z funkcji
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

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

type User = ReturnType<typeof getUser>;
// { id: 1, name: "John" }

// Wyciągnij typ argumentów
type Parameters<T> = T extends (...args: infer P) => any ? P : never;

function greet(name: string, age: number): void {}

type GreetParams = Parameters<typeof greet>;
// [string, number]

// Wyciągnij typ elementu tablicy
type ArrayElement<T> = T extends (infer E)[] ? E : never;

type Element = ArrayElement<string[]>; // string
type Element2 = ArrayElement<(number | boolean)[]>; // number | boolean

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

type Result = Awaited<Promise<string>>; // string
type Result2 = Awaited<string>; // string (nie Promise)

// Wyciągnij typ pierwszego argumentu
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;

type First = FirstArg<(a: string, b: number) => void>; // string

Zaawansowane użycie infer

// Wyciągnij typ z template literal
type ExtractRouteParams<T extends string> =
  T extends `${infer Start}:${infer Param}/${infer Rest}`
    ? Param | ExtractRouteParams<Rest>
    : T extends `${infer Start}:${infer Param}`
      ? Param
      : never;

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

// Wyciągnij typ z obiektu
type PropertyType<T, K extends keyof T> = T extends { [P in K]: infer V } ? V : never;

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

type NameType = PropertyType<User, 'name'>; // string

// Infer w pozycji covariant vs contravariant
type GetReturnType<T> = T extends (...args: never[]) => infer R ? R : never;

// Wiele infer - union dla covariant, intersection dla contravariant
type Foo<T> = T extends { a: infer U; b: infer U } ? U : never;
type Result = Foo<{ a: string; b: number }>; // string | number

type Bar<T> = T extends { a: (x: infer U) => void; b: (x: infer U) => void } ? U : never;
type Result2 = Bar<{ a: (x: string) => void; b: (x: number) => void }>; // string & number = never

Mapped Types

Jak działają mapped types?

Mapped types transformują właściwości istniejącego typu, iterując po jego kluczach:

// Podstawowa składnia
type Mapped<T> = {
  [K in keyof T]: T[K];
};

// Partial - wszystkie właściwości opcjonalne
type Partial<T> = {
  [K in keyof T]?: T[K];
};

// Required - wszystkie właściwości wymagane
type Required<T> = {
  [K in keyof T]-?: T[K]; // -? usuwa opcjonalność
};

// Readonly - wszystkie właściwości readonly
type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

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

// Przykład użycia
interface User {
  id: number;
  name: string;
  email?: string;
}

type PartialUser = Partial<User>;
// { id?: number; name?: string; email?: string }

type RequiredUser = Required<User>;
// { id: number; name: string; email: string }

type ReadonlyUser = Readonly<User>;
// { readonly id: number; readonly name: string; readonly email?: string }

Jak modyfikować klucze w mapped types (key remapping)?

Od TypeScript 4.1 możesz używać as do transformacji kluczy:

// Prefixowanie kluczy
type Prefixed<T, P extends string> = {
  [K in keyof T as `${P}${Capitalize<string & K>}`]: T[K];
};

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

type PrefixedUser = Prefixed<User, 'user'>;
// { userName: string; userAge: number }

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

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

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

// Gettery i settery
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;
};

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

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

// Usuwanie kluczy (mapowanie na never)
type OmitByType<T, U> = {
  [K in keyof T as T[K] extends U ? never : K]: T[K];
};

type WithoutNumbers = OmitByType<Mixed, number>;
// { name: string; email: string }

Pick, Omit, Record - jak działają?

// Pick - wybierz tylko określone klucze
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

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

type PublicUser = Pick<User, 'id' | 'name' | 'email'>;
// { id: number; name: string; email: string }

// Omit - usuń określone klucze
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

type UserWithoutPassword = Omit<User, 'password'>;
// { id: number; name: string; email: string }

// Record - utwórz typ z określonymi kluczami i wartościami
type Record<K extends keyof any, T> = {
  [P in K]: T;
};

type UserRoles = Record<'admin' | 'user' | 'guest', boolean>;
// { admin: boolean; user: boolean; guest: boolean }

// Dynamiczne klucze
type PageInfo = Record<string, { title: string; url: string }>;
const pages: PageInfo = {
  home: { title: 'Home', url: '/' },
  about: { title: 'About', url: '/about' }
};

Rekurencyjne mapped types

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

interface Config {
  database: {
    host: string;
    port: number;
    credentials: {
      username: string;
      password: string;
    };
  };
}

type PartialConfig = DeepPartial<Config>;
// Wszystkie zagnieżdżone właściwości są opcjonalne

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

// Deep Required
type DeepRequired<T> = {
  [K in keyof T]-?: T[K] extends object
    ? DeepRequired<T[K]>
    : T[K];
};

// Flatten nested objects
type Flatten<T> = T extends object
  ? { [K in keyof T]: T[K] extends object ? Flatten<T[K]> : T[K] }[keyof T]
  : T;

Template Literal Types

Jak działają template literal types?

Template literal types pozwalają tworzyć typy stringów przez konkatenację:

// Podstawowa konkatenacja
type Greeting = `Hello, ${string}`;
const greeting: Greeting = "Hello, World"; // ✅
// const bad: Greeting = "Hi, World"; // ❌

// Union w template
type Color = 'red' | 'blue' | 'green';
type Size = 'small' | 'medium' | 'large';

type ColoredSize = `${Color}-${Size}`;
// "red-small" | "red-medium" | "red-large" | "blue-small" | ...
// 9 kombinacji (3 × 3)

// Event handlers
type EventName = 'click' | 'hover' | 'focus';
type EventHandler = `on${Capitalize<EventName>}`;
// "onClick" | "onHover" | "onFocus"

// HTTP methods
type Method = 'get' | 'post' | 'put' | 'delete';
type Endpoint = '/users' | '/posts';
type Route = `${Uppercase<Method>} ${Endpoint}`;
// "GET /users" | "GET /posts" | "POST /users" | ...

Intrinsic string manipulation types

TypeScript ma wbudowane typy do manipulacji stringów:

// Uppercase - wszystkie litery wielkie
type Upper = Uppercase<'hello'>; // "HELLO"

// Lowercase - wszystkie litery małe
type Lower = Lowercase<'HELLO'>; // "hello"

// Capitalize - pierwsza litera wielka
type Cap = Capitalize<'hello'>; // "Hello"

// Uncapitalize - pierwsza litera mała
type Uncap = Uncapitalize<'Hello'>; // "hello"

// Praktyczne zastosowanie
type PropToGetter<T extends string> = `get${Capitalize<T>}`;
type PropToSetter<T extends string> = `set${Capitalize<T>}`;

type NameGetter = PropToGetter<'name'>; // "getName"
type NameSetter = PropToSetter<'name'>; // "setName"

// CSS-in-JS property types
type CSSProperty = 'margin' | 'padding' | 'border';
type CSSDirection = 'Top' | 'Right' | 'Bottom' | 'Left';

type CSSPropertyWithDirection = `${CSSProperty}${CSSDirection}`;
// "marginTop" | "marginRight" | ... | "borderLeft"

Parsowanie template literal types z infer

// Wyciąganie części stringa
type ExtractName<T extends string> =
  T extends `${infer First} ${infer Last}`
    ? { first: First; last: Last }
    : never;

type Name = ExtractName<"John Doe">;
// { first: "John"; last: "Doe" }

// Parsowanie route params
type ParseRoute<T extends string> =
  T extends `${infer _Start}:${infer Param}/${infer Rest}`
    ? { [K in Param]: string } & ParseRoute<`/${Rest}`>
    : T extends `${infer _Start}:${infer Param}`
      ? { [K in Param]: string }
      : {};

type RouteParams = ParseRoute<"/users/:userId/posts/:postId">;
// { userId: string } & { postId: string }

// Parsowanie query string
type ParseQuery<T extends string> =
  T extends `${infer Key}=${infer Value}&${infer Rest}`
    ? { [K in Key]: Value } & ParseQuery<Rest>
    : T extends `${infer Key}=${infer Value}`
      ? { [K in Key]: Value }
      : {};

type Query = ParseQuery<"name=John&age=30">;
// { name: "John" } & { age: "30" }

// Konwersja kebab-case na camelCase
type KebabToCamel<S extends string> =
  S extends `${infer First}-${infer Rest}`
    ? `${First}${Capitalize<KebabToCamel<Rest>>}`
    : S;

type Camel = KebabToCamel<"background-color">; // "backgroundColor"

Utility Types

Wbudowane utility types - przegląd

// Partial<T> - wszystkie właściwości opcjonalne
interface User { id: number; name: string; }
type PartialUser = Partial<User>; // { id?: number; name?: string }

// Required<T> - wszystkie właściwości wymagane
interface Config { host?: string; port?: number; }
type RequiredConfig = Required<Config>; // { host: string; port: number }

// Readonly<T> - wszystkie właściwości readonly
type ReadonlyUser = Readonly<User>; // { readonly id: number; readonly name: string }

// Record<K, T> - obiekt z kluczami K i wartościami T
type UserById = Record<number, User>; // { [key: number]: User }

// Pick<T, K> - wybierz klucze
type UserName = Pick<User, 'name'>; // { name: string }

// Omit<T, K> - usuń klucze
type UserWithoutId = Omit<User, 'id'>; // { name: string }

// Exclude<T, U> - usuń z union
type T1 = Exclude<'a' | 'b' | 'c', 'a'>; // 'b' | 'c'

// Extract<T, U> - zachowaj z union
type T2 = Extract<'a' | 'b' | 'c', 'a' | 'f'>; // 'a'

// NonNullable<T> - usuń null i undefined
type T3 = NonNullable<string | null | undefined>; // string

// ReturnType<T> - typ zwracany funkcji
function getUser() { return { id: 1, name: "John" }; }
type UserType = ReturnType<typeof getUser>; // { id: number; name: string }

// Parameters<T> - tuple typów argumentów
type GetUserParams = Parameters<typeof getUser>; // []

// ConstructorParameters<T> - argumenty konstruktora
class Person { constructor(name: string, age: number) {} }
type PersonArgs = ConstructorParameters<typeof Person>; // [string, number]

// InstanceType<T> - typ instancji klasy
type PersonInstance = InstanceType<typeof Person>; // Person

// ThisParameterType<T> - typ this funkcji
function fn(this: { name: string }) { return this.name; }
type ThisType = ThisParameterType<typeof fn>; // { name: string }

// OmitThisParameter<T> - usuń this z funkcji
type FnWithoutThis = OmitThisParameter<typeof fn>; // () => string

Tworzenie własnych utility types

// Nullable - dodaj null
type Nullable<T> = T | null;

// Maybe - dodaj null i undefined
type Maybe<T> = T | null | undefined;

// ValueOf - typ wartości obiektu
type ValueOf<T> = T[keyof T];

interface Colors { red: '#ff0000'; blue: '#0000ff'; }
type ColorValue = ValueOf<Colors>; // '#ff0000' | '#0000ff'

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

// Mutable - usuń readonly
type Mutable<T> = {
  -readonly [P in keyof T]: T[P];
};

// PickByType - wybierz właściwości określonego typu
type PickByType<T, U> = {
  [P in keyof T as T[P] extends U ? P : never]: T[P];
};

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

type NumberProps = PickByType<Mixed, number>;
// { age: number; count: number }

// OmitByType - usuń właściwości określonego typu
type OmitByType<T, U> = {
  [P in keyof T as T[P] extends U ? never : P]: T[P];
};

type NonNumberProps = OmitByType<Mixed, number>;
// { name: string; active: boolean }

// RequiredKeys - klucze wymagane
type RequiredKeys<T> = {
  [K in keyof T]-?: {} extends Pick<T, K> ? never : K;
}[keyof T];

// OptionalKeys - klucze opcjonalne
type OptionalKeys<T> = {
  [K in keyof T]-?: {} extends Pick<T, K> ? K : never;
}[keyof T];

interface Example {
  required: string;
  optional?: number;
}

type Req = RequiredKeys<Example>; // "required"
type Opt = OptionalKeys<Example>; // "optional"

Await i Promise utility types

// Awaited - rozpakuj Promise (wbudowany od TS 4.5)
type Awaited<T> =
  T extends null | undefined ? T :
  T extends object & { then(onfulfilled: infer F): any } ?
    F extends ((value: infer V) => any) ?
      Awaited<V> :
      never :
    T;

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

// PromiseType - wyciągnij typ z Promise
type PromiseType<T extends Promise<any>> =
  T extends Promise<infer U> ? U : never;

type PT = PromiseType<Promise<{ id: number }>>; // { id: number }

// MaybePromise - typ który może być Promise
type MaybePromise<T> = T | Promise<T>;

async function processData(data: MaybePromise<string>): Promise<string> {
  const resolved = await data;
  return resolved.toUpperCase();
}

Type Guards i Narrowing

Jak działają type guards?

Type guards to wyrażenia które zawężają typ w określonym zakresie:

// typeof guard
function process(value: string | number) {
  if (typeof value === 'string') {
    // value: string
    return value.toUpperCase();
  } else {
    // value: number
    return value.toFixed(2);
  }
}

// instanceof guard
class Dog { bark() {} }
class Cat { meow() {} }

function makeSound(animal: Dog | Cat) {
  if (animal instanceof Dog) {
    animal.bark(); // animal: Dog
  } else {
    animal.meow(); // animal: Cat
  }
}

// in guard
interface Fish { swim: () => void }
interface Bird { fly: () => void }

function move(animal: Fish | Bird) {
  if ('swim' in animal) {
    animal.swim(); // animal: Fish
  } else {
    animal.fly(); // animal: Bird
  }
}

// Equality narrowing
function example(x: string | number, y: string | boolean) {
  if (x === y) {
    // x: string, y: string (jedyny wspólny typ)
    console.log(x.toUpperCase());
  }
}

Jak tworzyć custom type guards?

// Type predicate: `value is Type`
function isString(value: unknown): value is string {
  return typeof value === 'string';
}

function process(value: unknown) {
  if (isString(value)) {
    // value: string
    console.log(value.toUpperCase());
  }
}

// Guard dla interfejsów
interface User {
  id: number;
  name: string;
  email: string;
}

interface Admin extends User {
  permissions: string[];
}

function isAdmin(user: User): user is Admin {
  return 'permissions' in user && Array.isArray((user as Admin).permissions);
}

function handleUser(user: User) {
  if (isAdmin(user)) {
    console.log(user.permissions); // user: Admin
  }
}

// Guard dla API responses
interface SuccessResponse<T> {
  success: true;
  data: T;
}

interface ErrorResponse {
  success: false;
  error: string;
}

type ApiResponse<T> = SuccessResponse<T> | ErrorResponse;

function isSuccess<T>(response: ApiResponse<T>): response is SuccessResponse<T> {
  return response.success === true;
}

async function fetchUser(id: number) {
  const response: ApiResponse<User> = await api.get(`/users/${id}`);

  if (isSuccess(response)) {
    return response.data; // response: SuccessResponse<User>
  } else {
    throw new Error(response.error); // response: ErrorResponse
  }
}

Assertion functions

// Assertion function - rzuca błąd jeśli warunek nie jest spełniony
function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== 'string') {
    throw new Error('Value is not a string');
  }
}

function process(value: unknown) {
  assertIsString(value);
  // Po assertion, value: string
  console.log(value.toUpperCase());
}

// Assertion z warunkiem
function assertDefined<T>(value: T | null | undefined): asserts value is T {
  if (value === null || value === undefined) {
    throw new Error('Value is not defined');
  }
}

function getUser(id: number) {
  const user = users.get(id);
  assertDefined(user);
  return user; // user: User (nie User | undefined)
}

// Assert non-null
function assertNonNull<T>(value: T | null, message?: string): asserts value is T {
  if (value === null) {
    throw new Error(message ?? 'Value is null');
  }
}

Discriminated Unions

Czym są discriminated unions?

Discriminated unions to union types z wspólną właściwością "discriminant" która pozwala TypeScript zawęzić typ:

// Discriminant: type property
interface Circle {
  type: 'circle';
  radius: number;
}

interface Rectangle {
  type: 'rectangle';
  width: number;
  height: number;
}

interface Triangle {
  type: 'triangle';
  base: number;
  height: number;
}

type Shape = Circle | Rectangle | Triangle;

function getArea(shape: Shape): number {
  switch (shape.type) {
    case 'circle':
      // shape: Circle
      return Math.PI * shape.radius ** 2;
    case 'rectangle':
      // shape: Rectangle
      return shape.width * shape.height;
    case 'triangle':
      // shape: Triangle
      return (shape.base * shape.height) / 2;
  }
}

// Exhaustiveness checking
function assertNever(x: never): never {
  throw new Error(`Unexpected value: ${x}`);
}

function getAreaExhaustive(shape: Shape): number {
  switch (shape.type) {
    case 'circle':
      return Math.PI * shape.radius ** 2;
    case 'rectangle':
      return shape.width * shape.height;
    case 'triangle':
      return (shape.base * shape.height) / 2;
    default:
      // Jeśli dodasz nowy typ do Shape, TypeScript pokaże błąd tutaj
      return assertNever(shape);
  }
}

Praktyczne zastosowania discriminated unions

// Redux-style actions
interface AddTodoAction {
  type: 'ADD_TODO';
  payload: { text: string };
}

interface ToggleTodoAction {
  type: 'TOGGLE_TODO';
  payload: { id: number };
}

interface DeleteTodoAction {
  type: 'DELETE_TODO';
  payload: { id: number };
}

type TodoAction = AddTodoAction | ToggleTodoAction | DeleteTodoAction;

function todoReducer(state: Todo[], action: TodoAction): Todo[] {
  switch (action.type) {
    case 'ADD_TODO':
      return [...state, { id: Date.now(), text: action.payload.text, done: false }];
    case 'TOGGLE_TODO':
      return state.map(todo =>
        todo.id === action.payload.id ? { ...todo, done: !todo.done } : todo
      );
    case 'DELETE_TODO':
      return state.filter(todo => todo.id !== action.payload.id);
  }
}

// API response handling
interface LoadingState {
  status: 'loading';
}

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

interface ErrorState {
  status: 'error';
  error: string;
}

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

function renderData<T>(state: AsyncState<T>, render: (data: T) => JSX.Element) {
  switch (state.status) {
    case 'loading':
      return <Spinner />;
    case 'success':
      return render(state.data);
    case 'error':
      return <Error message={state.error} />;
  }
}

// Form validation
interface ValidForm {
  isValid: true;
  data: FormData;
}

interface InvalidForm {
  isValid: false;
  errors: Record<string, string>;
}

type FormState = ValidForm | InvalidForm;

function submitForm(form: FormState) {
  if (form.isValid) {
    api.submit(form.data);
  } else {
    displayErrors(form.errors);
  }
}

Zaawansowane Patterns

Function Overloads

// Overloads dla różnych kombinacji argumentów
function createElement(tag: 'a'): HTMLAnchorElement;
function createElement(tag: 'canvas'): HTMLCanvasElement;
function createElement(tag: 'div'): HTMLDivElement;
function createElement(tag: string): HTMLElement;
function createElement(tag: string): HTMLElement {
  return document.createElement(tag);
}

const anchor = createElement('a');    // HTMLAnchorElement
const canvas = createElement('canvas'); // HTMLCanvasElement
const div = createElement('div');     // HTMLDivElement
const span = createElement('span');   // HTMLElement

// Overloads z różnymi typami zwracanymi
function processInput(input: string): string[];
function processInput(input: number): number;
function processInput(input: string | number): string[] | number {
  if (typeof input === 'string') {
    return input.split('');
  }
  return input * 2;
}

const chars = processInput('hello'); // string[]
const doubled = processInput(5);      // number

// Overloads z opcjonalnymi parametrami
function createUser(name: string): User;
function createUser(name: string, age: number): User;
function createUser(name: string, age: number, email: string): User;
function createUser(name: string, age?: number, email?: string): User {
  return { name, age: age ?? 0, email: email ?? '' };
}

Variance - covariance i contravariance

// Covariance - typ może być bardziej specific
// Działa dla typów zwracanych i readonly właściwości
interface Animal { name: string }
interface Dog extends Animal { bark(): void }

type CovariantFn = () => Animal;
const getDog: CovariantFn = (): Dog => ({ name: 'Rex', bark: () => {} }); // ✅

// Contravariance - typ może być bardziej general
// Działa dla parametrów funkcji
type ContravariantFn = (dog: Dog) => void;
const handleAnimal: ContravariantFn = (animal: Animal) => console.log(animal.name); // ✅

// Invariance - typ musi być dokładnie taki sam
// Działa dla mutable właściwości
interface Box<T> {
  value: T; // mutable = invariant
}

// const dogBox: Box<Animal> = { value: { name: 'Rex', bark: () => {} } as Dog }; // działa
// Ale nie możesz przypisać Box<Dog> do Box<Animal> ani odwrotnie

// Bivariance (tylko z --strictFunctionTypes: false)
// Funkcje są bivariant w parametrach (legacy behavior)

// Praktyczne zastosowanie
interface Repository<T> {
  // Covariant - zwracany typ
  find(id: number): T;

  // Contravariant - parametr
  save(entity: T): void;
}

// Z modyfikatorami
interface ReadonlyRepository<out T> {
  // 'out' oznacza covariant
  find(id: number): T;
}

interface WriteRepository<in T> {
  // 'in' oznacza contravariant
  save(entity: T): void;
}

Branded Types (Nominal Types)

// TypeScript używa structural typing
// Branded types dodają "nominal" typing

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

type USD = Brand<number, 'USD'>;
type EUR = Brand<number, 'EUR'>;

const usd = 100 as USD;
const eur = 100 as EUR;

function addUSD(a: USD, b: USD): USD {
  return (a + b) as USD;
}

addUSD(usd, usd); // ✅
// addUSD(usd, eur); // ❌ Type 'EUR' is not assignable to type 'USD'

// Validated types
type Email = Brand<string, 'Email'>;
type UserId = Brand<number, 'UserId'>;

function validateEmail(input: string): Email | null {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(input) ? (input as Email) : null;
}

function sendEmail(to: Email, subject: string): void {
  // Mamy pewność że to jest zwalidowany email
}

const maybeEmail = validateEmail('test@example.com');
if (maybeEmail) {
  sendEmail(maybeEmail, 'Hello'); // ✅
}
// sendEmail('invalid', 'Hello'); // ❌ string is not Email

// Unique symbols (alternatywa)
declare const userIdSymbol: unique symbol;
type UserIdAlt = number & { [userIdSymbol]: never };

Builder Pattern z TypeScript

// Type-safe builder
interface UserBuilder {
  name: string;
  email?: string;
  age?: number;
}

type BuilderMethods<T, K extends keyof T = keyof T> = {
  [P in K]-?: (value: T[P]) => BuilderResult<T, Exclude<K, P>>;
};

type BuilderResult<T, K extends keyof T = keyof T> =
  K extends never
    ? { build(): T }
    : BuilderMethods<T, K> & { build(): T };

function createBuilder<T>(): BuilderResult<T, keyof T> {
  const state: Partial<T> = {};

  const handler: ProxyHandler<any> = {
    get(_, prop) {
      if (prop === 'build') {
        return () => state as T;
      }
      return (value: any) => {
        (state as any)[prop] = value;
        return new Proxy({}, handler);
      };
    }
  };

  return new Proxy({}, handler);
}

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

const user = createBuilder<User>()
  .name('John')
  .email('john@example.com')
  .age(30)
  .build();

Zadania Praktyczne

1. Zaimplementuj DeepReadonly

// Zaimplementuj typ który rekurencyjnie dodaje readonly
type DeepReadonly<T> = /* twoja implementacja */;

// Powinno działać:
interface Config {
  database: {
    host: string;
    port: number;
    credentials: {
      username: string;
      password: string;
    };
  };
}

type ReadonlyConfig = DeepReadonly<Config>;
// Wszystkie właściwości readonly, nawet zagnieżdżone

2. Zaimplementuj PartialByKeys

// Zaimplementuj typ który robi określone klucze opcjonalne
type PartialByKeys<T, K extends keyof T> = /* twoja implementacja */;

// Powinno działać:
interface User {
  id: number;
  name: string;
  email: string;
}

type UserWithOptionalEmail = PartialByKeys<User, 'email'>;
// { id: number; name: string; email?: string }

3. Zaimplementuj PathValue

// Zaimplementuj typ który wyciąga typ z zagnieżdżonego path
type PathValue<T, P extends string> = /* twoja implementacja */;

// Powinno działać:
interface Data {
  user: {
    profile: {
      name: string;
      age: number;
    };
  };
}

type Name = PathValue<Data, 'user.profile.name'>; // string
type Age = PathValue<Data, 'user.profile.age'>;   // number

4. Zaimplementuj TupleToUnion

// Zaimplementuj typ który konwertuje tuple na union
type TupleToUnion<T extends readonly any[]> = /* twoja implementacja */;

// Powinno działać:
type Tuple = ['a', 'b', 'c'];
type Union = TupleToUnion<Tuple>; // 'a' | 'b' | 'c'

Podsumowanie

Zaawansowany TypeScript to potężne narzędzie do budowania type-safe aplikacji. Na rozmowach rekrutacyjnych skup się na:

  1. Generics - parametry typów, constraints, wielokrotne parametry
  2. Conditional types - extends, distributive, infer
  3. Mapped types - transformacje, key remapping, modyfikatory
  4. Template literal types - konkatenacja, parsowanie, intrinsic types
  5. Utility types - wbudowane i własne
  6. Type guards - narrowing, custom guards, assertions
  7. Discriminated unions - pattern matching, exhaustiveness

Praktyka to klucz - rozwiązuj zadania na type-challenges.github.io i analizuj typy w popularnych bibliotekach jak React, Redux, Zod.

Powrót do blogu

Zostaw komentarz

Pamiętaj, że komentarze muszą zostać zatwierdzone przed ich opublikowaniem.