Pytania Rekrutacyjne z TypeScript dla Początkujących

Sławomir Plamowski 28 min czytania
backend frontend interview-questions javascript poczatkujacy programowanie typescript

Adnotacje typów, interfejsy, generyki - dla programistów przechodzących z JavaScript na TypeScript te koncepty często wydają się przytłaczające. Pierwszy kontakt z lawiną czerwonych podkreśleń w edytorze potrafi zniechęcić. Ale właśnie ten początkowy dyskomfort jest najlepszą inwestycją w karierę - TypeScript nie jest trudny, wymaga tylko zmiany sposobu myślenia o kodzie.

Na rozmowach rekrutacyjnych TypeScript coraz częściej pojawia się jako standard, nie dodatek. Znajomość typów, interfejsów i podstawowych wzorców to dziś wymaganie na większości stanowisk frontendowych.

W tym artykule znajdziesz najważniejsze pytania rekrutacyjne z TypeScript dla początkujących. Bez zaawansowanych typów warunkowych czy skomplikowanej inferencji - skupimy się na fundamentach, które musisz znać, żeby przejść rozmowę na stanowisko junior lub regular developera.

1. Czym Jest TypeScript i Po Co Go Używamy?

To pytanie otwierające, które pojawia się na niemal każdej rozmowie. Rekruter chce sprawdzić, czy rozumiesz podstawową ideę TypeScript, czy tylko nauczyłeś się składni na pamięć.

Odpowiedź w 30 sekund

Gdy padnie to pytanie, odpowiadam tak:

TypeScript to nadzbiór JavaScript z opcjonalnym statycznym typowaniem. Każdy poprawny kod JavaScript jest poprawnym kodem TypeScript. Główna korzyść to wykrywanie błędów już podczas pisania kodu, a nie dopiero w runtime. Daje nam też lepsze podpowiadanie w edytorach i samodokumentujący się kod. TypeScript kompiluje się do zwykłego JavaScript.

Poczekaj na reakcję rekrutera. Jeśli chce więcej szczegółów, rozwiń odpowiedź.

Odpowiedź w 2 minuty

TypeScript został stworzony przez Microsoft w 2012 roku jako odpowiedź na problemy z utrzymaniem dużych aplikacji JavaScript. JavaScript jest świetny do prototypowania, ale w dużym projekcie brak typów prowadzi do trudnych do znalezienia błędów.

Pokażę na przykładzie, dlaczego to ma znaczenie:

// W czystym JavaScript - błąd ujawni się dopiero w runtime
function calculateTotal(items) {
    return items.reduce((sum, item) => sum + item.price, 0);
}

// Gdzieś w kodzie ktoś wywołuje:
calculateTotal("not an array");  // Crash w runtime!

// W TypeScript - błąd widoczny od razu w edytorze
interface CartItem {
    name: string;
    price: number;
}

function calculateTotal(items: CartItem[]): number {
    return items.reduce((sum, item) => sum + item.price, 0);
}

calculateTotal("not an array");  // Błąd kompilacji!
// Argument of type 'string' is not assignable to parameter of type 'CartItem[]'

Ta czerwona linia w edytorze pojawia się natychmiast, podczas pisania. Nie musisz uruchamiać aplikacji, żeby zobaczyć problem. W dużych projektach z setkami plików i dziesiątkami developerów to ogromna oszczędność czasu.

Dlaczego Rekruterzy O To Pytają

Chcą sprawdzić, czy rozumiesz wartość TypeScript, a nie tylko czy umiesz go używać. Kandydat, który mówi "bo tak teraz wszyscy piszą" nie robi dobrego wrażenia. Kandydat, który potrafi wyjaśnić konkretne problemy, które TypeScript rozwiązuje, pokazuje dojrzałość programistyczną.

2. Podstawowe Typy w TypeScript

Znajomość typów podstawowych to fundament. Rekruterzy często proszą o wymienienie typów lub pytają o różnice między nimi.

Odpowiedź w 30 sekund

TypeScript ma typy prymitywne jak string, number, boolean, null, undefined, symbol i bigint. Ma też typy specjalne: any (wyłącza typowanie), unknown (bezpieczna alternatywa dla any), void (brak wartości zwracanej), never (funkcja nigdy nie kończy działania). Dla kolekcji mamy Array i tuple. Do definiowania kształtu obiektów używamy interface lub type.

Przegląd Typów z Przykładami

Pokażę każdy typ w praktyce:

// Typy prymitywne
let name: string = "Anna";
let age: number = 25;
let isActive: boolean = true;
let nothing: null = null;
let notDefined: undefined = undefined;

// Tablice - dwa sposoby zapisu
let numbers: number[] = [1, 2, 3];
let names: Array<string> = ["Anna", "Bartek"];

// Tuple - tablica o ustalonej długości i typach
let person: [string, number] = ["Anna", 25];
// person[0] jest string, person[1] jest number

// Enum - zbiór nazwanych stałych
enum Color {
    Red,      // 0
    Green,    // 1
    Blue      // 2
}
let favoriteColor: Color = Color.Green;

// Enum z wartościami string
enum Direction {
    Up = "UP",
    Down = "DOWN",
    Left = "LEFT",
    Right = "RIGHT"
}

Typy Specjalne - Tu Robi Się Ciekawie

// any - wyłącza sprawdzanie typów (UNIKAJ!)
let anything: any = "tekst";
anything = 42;        // OK
anything = true;      // OK
anything.foo.bar;     // OK dla kompilatora, crash w runtime!

// unknown - bezpieczniejsza alternatywa
let uncertain: unknown = "tekst";
uncertain = 42;       // OK

// Ale nie możesz użyć bez sprawdzenia typu
// uncertain.toUpperCase();  // Błąd!

if (typeof uncertain === "string") {
    uncertain.toUpperCase();  // OK - TypeScript wie, że to string
}

// void - funkcja nic nie zwraca
function logMessage(message: string): void {
    console.log(message);
    // brak return
}

// never - funkcja nigdy nie kończy działania
function throwError(message: string): never {
    throw new Error(message);
}

function infiniteLoop(): never {
    while (true) {
        // nigdy się nie kończy
    }
}

Klasyczny Problem: any vs unknown

Rekruter może zapytać: "Kiedy używasz any, a kiedy unknown?"

Wzorzec, który mi się sprawdził:

Staram się nigdy nie używać any - to jak wyłączenie świateł w ciemnym pokoju pełnym mebli. Unknown używam gdy naprawdę nie wiem jaki typ dostanę, na przykład przy parsowaniu JSON z zewnętrznego API. Różnica jest taka, że unknown wymusza sprawdzenie typu przed użyciem, więc kompilator mnie ochroni.

// Dane z zewnętrznego API
async function fetchData(): Promise<unknown> {
    const response = await fetch('/api/data');
    return response.json();
}

async function processData() {
    const data = await fetchData();

    // Nie mogę od razu użyć data.name - muszę sprawdzić typ
    if (typeof data === 'object' && data !== null && 'name' in data) {
        console.log((data as { name: string }).name);
    }
}

3. Interface vs Type - Kiedy Którego Używać

To jedno z najczęstszych pytań na rozmowach. Oba mechanizmy wydają się robić to samo, więc rekruterzy sprawdzają, czy rozumiesz subtelne różnice.

Odpowiedź w 30 sekund

Oba definiują kształt danych, ale interface jest rozszerzalny przez extends i automatycznie łączy się z innymi o tej samej nazwie. Type jest bardziej elastyczny - obsługuje unie, przecięcia, typy prymitywne i tuple. W praktyce używam interface dla obiektów i API publicznych, type dla unii i złożonych typów.

Odpowiedź w 2 minuty

Pokażę kluczowe różnice:

// Interface - definiuje kształt obiektu
interface User {
    name: string;
    age: number;
}

// Type alias - też definiuje kształt
type UserType = {
    name: string;
    age: number;
};

// Do tego momentu działają identycznie
const user1: User = { name: "Anna", age: 25 };
const user2: UserType = { name: "Bartek", age: 30 };

Różnice pojawiają się przy bardziej zaawansowanych zastosowaniach:

// 1. Interface można rozszerzać przez extends
interface Animal {
    name: string;
}

interface Dog extends Animal {
    breed: string;
}

// Type używa przecięcia (&)
type AnimalType = {
    name: string;
};

type DogType = AnimalType & {
    breed: string;
};

// 2. Interface automatycznie się łączy (declaration merging)
interface Config {
    apiUrl: string;
}

interface Config {
    timeout: number;
}

// Config ma teraz oba pola: apiUrl i timeout
const config: Config = {
    apiUrl: "https://api.example.com",
    timeout: 5000
};

// Type nie pozwala na to - błąd duplikatu
// type Config = { apiUrl: string };
// type Config = { timeout: number };  // Błąd!

// 3. Type obsługuje unie i typy prymitywne
type Status = "pending" | "success" | "error";  // Union type
type ID = string | number;                       // Union type

// Interface nie może tego zrobić bezpośrednio

// 4. Type obsługuje tuple
type Point = [number, number];
type NamedPoint = [string, number, number];

Moja Praktyczna Zasada

Gdy piszę kod, stosuję prostą regułę:

// Interface dla "rzeczy" - obiektów, klas, API
interface UserRepository {
    findById(id: string): Promise<User>;
    save(user: User): Promise<void>;
}

interface ApiResponse<T> {
    data: T;
    status: number;
}

// Type dla "typów złożonych" - unii, wariantów, aliasów
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type Result<T> = { success: true; data: T } | { success: false; error: string };
type Nullable<T> = T | null;

4. Wnioskowanie Typów - Kiedy Pisać Typy, Kiedy Nie

TypeScript jest mądrzejszy niż się wydaje. Potrafi sam wywnioskować typy z kontekstu. Pytanie brzmi: kiedy polegać na wnioskowanie, a kiedy pisać typy jawnie?

Odpowiedź w 30 sekund

TypeScript automatycznie wnioskuje typy z przypisanych wartości i kontekstu. Nie muszę pisać let x: number = 5, bo kompilator wie, że 5 to number. Jawne adnotacje dodaję gdy: inicjalizuję pustą tablicę, funkcja ma złożony typ zwracany, chcę szerszy typ niż wnioskowany, lub dla czytelności w API publicznych.

Kiedy Wnioskowanie Działa Doskonale

// TypeScript wywnioskuje wszystkie typy
let name = "Anna";                    // string
let age = 25;                         // number
let isActive = true;                  // boolean
let numbers = [1, 2, 3];              // number[]
let user = { name: "Anna", age: 25 }; // { name: string; age: number }

// Wnioskowanie w funkcjach
function double(x: number) {
    return x * 2;  // TypeScript wie, że zwraca number
}

const result = double(5);  // result jest number

// Wnioskowanie z kontekstu (contextual typing)
const names = ["Anna", "Bartek", "Celina"];
names.map(name => name.toUpperCase());
// TypeScript wie, że name jest string

Kiedy Potrzebujesz Jawnych Typów

// 1. Pusta tablica - TypeScript nie wie co będzie w środku
const items: string[] = [];  // bez adnotacji byłoby never[]
items.push("first");

// 2. Późniejsza inicjalizacja
let user: User;
if (condition) {
    user = fetchUserFromCache();
} else {
    user = fetchUserFromApi();
}

// 3. Chcesz szerszy typ niż wnioskowany
let id: string | number = "abc";
id = 123;  // OK dzięki jawnej adnotacji

// Bez adnotacji:
let id2 = "abc";
// id2 = 123;  // Błąd! id2 jest string

// 4. Typy zwracane w funkcjach publicznych
function createUser(name: string, age: number): User {
    return { name, age, createdAt: new Date() };
}
// Jawny typ zwracany dokumentuje API

// 5. Parametry funkcji - zawsze wymagają typów
function greet(name: string): void {
    console.log(`Hello, ${name}`);
}

Wzorzec, Który Mi Się Sprawdził

Stosuję zasadę "typy na granicach":

// Jawne typy na granicach modułu (eksportowane funkcje)
export function calculateDiscount(price: number, percent: number): number {
    // Wewnątrz funkcji TypeScript sobie poradzi
    const discount = price * (percent / 100);
    const finalPrice = price - discount;
    return finalPrice;  // TypeScript wie, że to number
}

// Jawne typy dla danych zewnętrznych
interface ApiUser {
    id: string;
    name: string;
    email: string;
}

async function fetchUser(id: string): Promise<ApiUser> {
    const response = await fetch(`/api/users/${id}`);
    const data = await response.json();
    return data as ApiUser;  // Asercja typu dla danych z API
}

5. Funkcje w TypeScript - Typy Parametrów i Zwracane

Funkcje to chleb powszedni programisty. TypeScript daje nam pełną kontrolę nad typami argumentów i wartości zwracanych.

Odpowiedź w 30 sekund

W TypeScript każdy parametr funkcji wymaga typu. Typ zwracany jest wnioskowany, ale warto go podawać jawnie dla dokumentacji. Dla opcjonalnych parametrów używamy znaku zapytania lub wartości domyślnych. Obsługujemy też rest parameters i przeciążanie funkcji.

Podstawowa Składnia

// Funkcja z typami parametrów i zwracanym
function add(a: number, b: number): number {
    return a + b;
}

// Funkcja strzałkowa
const multiply = (a: number, b: number): number => a * b;

// Typ funkcji jako zmienna
type MathOperation = (a: number, b: number) => number;

const divide: MathOperation = (a, b) => a / b;

Parametry Opcjonalne i Domyślne

// Parametr opcjonalny - znak zapytania
function greet(name: string, greeting?: string): string {
    return `${greeting || "Hello"}, ${name}!`;
}

greet("Anna");           // "Hello, Anna!"
greet("Anna", "Hi");     // "Hi, Anna!"

// Parametr z wartością domyślną
function greetDefault(name: string, greeting: string = "Hello"): string {
    return `${greeting}, ${name}!`;
}

greetDefault("Anna");        // "Hello, Anna!"
greetDefault("Anna", "Hi");  // "Hi, Anna!"

// Uwaga: opcjonalny i domyślny to nie to samo!
// Opcjonalny może być undefined, domyślny nigdy

Rest Parameters

// Zbieranie wielu argumentów w tablicę
function sum(...numbers: number[]): number {
    return numbers.reduce((total, n) => total + n, 0);
}

sum(1, 2, 3);       // 6
sum(1, 2, 3, 4, 5); // 15

// Połączenie zwykłych i rest parameters
function logWithPrefix(prefix: string, ...messages: string[]): void {
    messages.forEach(msg => console.log(`[${prefix}] ${msg}`));
}

logWithPrefix("INFO", "Start", "Processing", "Done");

Przeciążanie Funkcji

Tu robi się ciekawie. TypeScript pozwala zdefiniować wiele sygnatur dla jednej funkcji:

// Sygnatury przeciążone
function format(value: string): string;
function format(value: number): string;
function format(value: Date): string;

// Implementacja (musi obsłużyć wszystkie przypadki)
function format(value: string | number | Date): string {
    if (typeof value === "string") {
        return value.toUpperCase();
    }
    if (typeof value === "number") {
        return value.toFixed(2);
    }
    return value.toISOString();
}

format("hello");        // "HELLO"
format(3.14159);        // "3.14"
format(new Date());     // "2025-12-27T..."

Klasyczny Problem: Callback z Typami

Rekruter może poprosić o napisanie funkcji przyjmującej callback:

// Funkcja z callbackiem
function processItems<T>(
    items: T[],
    callback: (item: T, index: number) => void
): void {
    items.forEach((item, index) => callback(item, index));
}

// Użycie
processItems(["a", "b", "c"], (item, index) => {
    console.log(`${index}: ${item}`);
});

// Bardziej złożony przykład - callback zwracający wartość
function mapItems<T, U>(
    items: T[],
    transform: (item: T) => U
): U[] {
    return items.map(transform);
}

const numbers = [1, 2, 3];
const strings = mapItems(numbers, n => n.toString());
// strings jest string[]

6. Interface - Definiowanie Kształtu Obiektów

Interface to podstawowe narzędzie do definiowania struktury danych w TypeScript. Na rozmowach rekrutacyjnych często sprawdzane są praktyczne zastosowania.

Odpowiedź w 30 sekund

Interface definiuje kontrakt - jakie właściwości i metody musi mieć obiekt. Może zawierać właściwości opcjonalne, readonly, i metody. Interfejsy można rozszerzać przez extends i łączyć przez declaration merging. Używam ich do typowania obiektów, propsów komponentów i kontraktów API.

Podstawowa Składnia

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

const user: User = {
    id: 1,
    name: "Anna",
    email: "anna@example.com"
};

// Brakująca właściwość - błąd
// const incomplete: User = { id: 1, name: "Anna" };
// Property 'email' is missing

// Dodatkowa właściwość - błąd
// const extra: User = { id: 1, name: "Anna", email: "...", age: 25 };
// Object literal may only specify known properties

Właściwości Opcjonalne i Readonly

interface Config {
    apiUrl: string;
    timeout?: number;           // opcjonalne
    readonly apiKey: string;    // tylko do odczytu
}

const config: Config = {
    apiUrl: "https://api.example.com",
    apiKey: "secret123"
    // timeout pominięte - OK
};

// config.apiKey = "newKey";  // Błąd! Cannot assign to 'apiKey'
config.apiUrl = "new-url";    // OK - nie jest readonly

Metody w Interface

interface Calculator {
    // Metoda z pełną składnią
    add(a: number, b: number): number;

    // Metoda jako właściwość funkcyjna
    subtract: (a: number, b: number) => number;

    // Właściwość readonly z metodą
    readonly multiply: (a: number, b: number) => number;
}

const calc: Calculator = {
    add(a, b) {
        return a + b;
    },
    subtract: (a, b) => a - b,
    multiply: (a, b) => a * b
};

Rozszerzanie Interfejsów

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

interface Pet extends Animal {
    owner: string;
}

interface Dog extends Pet {
    breed: string;
    bark(): void;
}

const myDog: Dog = {
    name: "Rex",
    age: 3,
    owner: "Anna",
    breed: "German Shepherd",
    bark() {
        console.log("Woof!");
    }
};

// Rozszerzanie wielu interfejsów
interface FlyingAnimal extends Animal {
    wingspan: number;
}

interface Parrot extends Pet, FlyingAnimal {
    vocabulary: string[];
}

Index Signatures - Dynamiczne Klucze

// Obiekt z dowolnymi kluczami string
interface Dictionary {
    [key: string]: string;
}

const translations: Dictionary = {
    hello: "cześć",
    goodbye: "do widzenia",
    thanks: "dziękuję"
};

// Połączenie ze znanymi właściwościami
interface UserData {
    id: number;
    name: string;
    [key: string]: string | number;  // dodatkowe właściwości
}

const userData: UserData = {
    id: 1,
    name: "Anna",
    role: "admin",
    department: "IT"
};

7. Generyki - Wielokrotnego Użytku Komponenty z Typami

Generyki to jeden z najpotężniejszych mechanizmów TypeScript. Pozwalają pisać kod, który działa z wieloma typami, zachowując pełne bezpieczeństwo typów.

Odpowiedź w 30 sekund

Generyki to parametry typów - jak argumenty funkcji, ale dla typów. Zamiast tracić typowanie przez any, przekazujemy typ jako parametr. Przykład: Array<number> to tablica z generykiem. Używam ich gdy piszę funkcje lub klasy, które mają działać z różnymi typami danych zachowując informację o typie.

Odpowiedź w 2 minuty

Zacznijmy od problemu, który generyki rozwiązują:

// Bez generyków - tracimy informację o typie
function firstElement(arr: any[]): any {
    return arr[0];
}

const num = firstElement([1, 2, 3]);
// num jest any - TypeScript nie wie, że to number

// Z generykami - zachowujemy typ
function firstElementTyped<T>(arr: T[]): T {
    return arr[0];
}

const numTyped = firstElementTyped([1, 2, 3]);
// numTyped jest number!

const strTyped = firstElementTyped(["a", "b", "c"]);
// strTyped jest string!

Generyki w Funkcjach

// Prosty generyk
function identity<T>(value: T): T {
    return value;
}

// TypeScript wnioskuje typ z argumentu
const str = identity("hello");  // T = string
const num = identity(42);       // T = number

// Możesz też podać typ jawnie
const explicit = identity<boolean>(true);

// Wiele parametrów generycznych
function pair<K, V>(key: K, value: V): [K, V] {
    return [key, value];
}

const kvPair = pair("name", "Anna");  // [string, string]
const mixedPair = pair(1, true);      // [number, boolean]

Ograniczenia Generyków (Constraints)

Czasem chcemy ograniczyć, jakie typy są dozwolone:

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

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

// T musi rozszerzać konkretny interface
interface HasId {
    id: number;
}

function findById<T extends HasId>(items: T[], id: number): T | undefined {
    return items.find(item => item.id === id);
}

const users = [
    { id: 1, name: "Anna" },
    { id: 2, name: "Bartek" }
];

const found = findById(users, 1);
// found jest { id: number; name: string } | undefined

Generyki w Interfejsach i Typach

// Interface z generykiem
interface Response<T> {
    data: T;
    status: number;
    message: string;
}

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

const userResponse: Response<User> = {
    data: { id: 1, name: "Anna" },
    status: 200,
    message: "Success"
};

// Type z generykiem
type Result<T> =
    | { success: true; data: T }
    | { success: false; error: string };

function fetchUser(id: number): Result<User> {
    if (id > 0) {
        return { success: true, data: { id, name: "User " + id } };
    }
    return { success: false, error: "Invalid ID" };
}

Wzorzec, Który Mi Się Sprawdził

Używam generyków gdy piszę "pojemniki" na dane:

// Generyczna klasa Repository
class Repository<T extends HasId> {
    private items: T[] = [];

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

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

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

// Użycie dla różnych typów
interface Product extends HasId {
    id: number;
    name: string;
    price: number;
}

const userRepo = new Repository<User>();
const productRepo = new Repository<Product>();

userRepo.add({ id: 1, name: "Anna" });
productRepo.add({ id: 1, name: "Laptop", price: 2999 });

8. Unie i Typy Literałowe - Precyzyjne Typowanie

Typy unii pozwalają wyrażać, że wartość może być jednym z kilku typów. Typy literałowe ograniczają dozwolone wartości do konkretnych.

Odpowiedź w 30 sekund

Union type używa pionowej kreski do połączenia typów - string | number oznacza, że wartość może być stringiem lub numberem. Typy literałowe to konkretne wartości jako typy - 'success' | 'error' zamiast ogólnego string. Razem dają precyzyjne typowanie dla stanów, statusów i wariantów.

Podstawowe Unie

// Zmienna może być jednym z typów
let id: string | number;
id = "abc123";  // OK
id = 42;        // OK
// id = true;   // Błąd!

// W funkcjach
function formatId(id: string | number): string {
    // TypeScript wymaga obsługi obu przypadków
    if (typeof id === "string") {
        return id.toUpperCase();
    }
    return id.toString();
}

// Z tablicami
let mixed: (string | number)[] = [1, "two", 3, "four"];

Typy Literałowe

// Literał string
type Direction = "north" | "south" | "east" | "west";

function move(direction: Direction): void {
    console.log(`Moving ${direction}`);
}

move("north");  // OK
// move("up");  // Błąd! Type '"up"' is not assignable to type 'Direction'

// Literał number
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;

function rollDice(): DiceRoll {
    return Math.ceil(Math.random() * 6) as DiceRoll;
}

// Połączenie z obiektami - discriminated unions
type Result =
    | { status: "success"; data: string }
    | { status: "error"; error: string }
    | { status: "loading" };

function handleResult(result: Result): void {
    switch (result.status) {
        case "success":
            console.log(result.data);    // TypeScript wie, że jest data
            break;
        case "error":
            console.log(result.error);   // TypeScript wie, że jest error
            break;
        case "loading":
            console.log("Loading...");
            break;
    }
}

Praktyczny Przykład: Status Zamówienia

type OrderStatus = "pending" | "confirmed" | "shipped" | "delivered" | "cancelled";

interface Order {
    id: string;
    status: OrderStatus;
    items: string[];
}

function canCancel(order: Order): boolean {
    // Tylko pending i confirmed można anulować
    return order.status === "pending" || order.status === "confirmed";
}

function updateStatus(order: Order, newStatus: OrderStatus): Order {
    // TypeScript sprawdzi, czy newStatus jest poprawny
    return { ...order, status: newStatus };
}

// Błąd kompilacji jeśli użyjemy niepoprawnego statusu
// updateStatus(order, "unknown");

Type Guards - Zawężanie Typów

// typeof guard
function process(value: string | number): void {
    if (typeof value === "string") {
        // TypeScript wie, że tutaj value jest string
        console.log(value.toUpperCase());
    } else {
        // TypeScript wie, że tutaj value jest number
        console.log(value.toFixed(2));
    }
}

// in guard
interface Bird {
    fly(): void;
    layEggs(): void;
}

interface Fish {
    swim(): void;
    layEggs(): void;
}

function move(animal: Bird | Fish): void {
    if ("fly" in animal) {
        animal.fly();  // TypeScript wie, że to Bird
    } else {
        animal.swim(); // TypeScript wie, że to Fish
    }
}

// Własny type guard
function isString(value: unknown): value is string {
    return typeof value === "string";
}

function processUnknown(value: unknown): void {
    if (isString(value)) {
        // TypeScript wie, że value jest string
        console.log(value.toUpperCase());
    }
}

9. Klasy w TypeScript

TypeScript rozszerza klasy JavaScript o typy, modyfikatory dostępu i inne funkcjonalności znane z języków obiektowych.

Odpowiedź w 30 sekund

Klasy w TypeScript mają modyfikatory dostępu: public (domyślny), private (tylko w klasie), protected (klasa i podklasy). Możemy oznaczać właściwości jako readonly. Mamy też skrót w konstruktorze do deklaracji właściwości. Klasy mogą implementować interfejsy przez implements.

Podstawowa Składnia

class User {
    // Właściwości z typami
    id: number;
    name: string;
    private password: string;
    readonly createdAt: Date;

    constructor(id: number, name: string, password: string) {
        this.id = id;
        this.name = name;
        this.password = password;
        this.createdAt = new Date();
    }

    // Metoda
    greet(): string {
        return `Hello, ${this.name}!`;
    }

    // Metoda prywatna
    private hashPassword(): string {
        return `hashed_${this.password}`;
    }
}

const user = new User(1, "Anna", "secret123");
console.log(user.name);         // OK
// console.log(user.password);  // Błąd! Property 'password' is private
// user.createdAt = new Date(); // Błąd! Cannot assign to 'createdAt'

Skrót w Konstruktorze

TypeScript pozwala deklarować właściwości bezpośrednio w parametrach konstruktora:

// Zamiast tego:
class UserLong {
    public id: number;
    public name: string;
    private email: string;

    constructor(id: number, name: string, email: string) {
        this.id = id;
        this.name = name;
        this.email = email;
    }
}

// Możesz napisać:
class UserShort {
    constructor(
        public id: number,
        public name: string,
        private email: string
    ) {}
    // Właściwości są automatycznie utworzone i przypisane
}

Modyfikatory Dostępu

class Animal {
    public name: string;           // dostępne wszędzie
    protected species: string;     // dostępne w klasie i podklasach
    private _id: number;           // dostępne tylko w tej klasie

    constructor(name: string, species: string, id: number) {
        this.name = name;
        this.species = species;
        this._id = id;
    }

    // Getter i setter
    get id(): number {
        return this._id;
    }
}

class Dog extends Animal {
    constructor(name: string, id: number) {
        super(name, "Canis familiaris", id);
    }

    describe(): string {
        // Dostęp do protected jest OK
        return `${this.name} is a ${this.species}`;
        // this._id - Błąd! private nie jest dostępne
    }
}

const dog = new Dog("Rex", 1);
console.log(dog.name);     // OK - public
// console.log(dog.species); // Błąd! - protected

Implementowanie Interfejsów

interface Printable {
    print(): void;
}

interface Serializable {
    toJSON(): string;
}

class Document implements Printable, Serializable {
    constructor(
        private title: string,
        private content: string
    ) {}

    print(): void {
        console.log(`=== ${this.title} ===`);
        console.log(this.content);
    }

    toJSON(): string {
        return JSON.stringify({ title: this.title, content: this.content });
    }
}

Klasy Abstrakcyjne

abstract class Shape {
    constructor(protected color: string) {}

    // Metoda abstrakcyjna - musi być zaimplementowana
    abstract calculateArea(): number;

    // Zwykła metoda - może być używana bezpośrednio
    describe(): string {
        return `A ${this.color} shape with area ${this.calculateArea()}`;
    }
}

class Circle extends Shape {
    constructor(color: string, private radius: number) {
        super(color);
    }

    calculateArea(): number {
        return Math.PI * this.radius ** 2;
    }
}

class Rectangle extends Shape {
    constructor(color: string, private width: number, private height: number) {
        super(color);
    }

    calculateArea(): number {
        return this.width * this.height;
    }
}

// const shape = new Shape("red");  // Błąd! Cannot create instance of abstract class
const circle = new Circle("red", 5);
console.log(circle.describe());  // "A red shape with area 78.54..."

10. Konfiguracja TypeScript - tsconfig.json

Rozumienie konfiguracji to znak dojrzałego programisty. Rekruterzy często pytają o najważniejsze opcje.

Odpowiedź w 30 sekund

tsconfig.json to plik konfiguracyjny TypeScript. Kluczowe opcje: target (docelowa wersja JS), strict (włącza wszystkie sprawdzenia), module (format modułów), outDir (folder wyjściowy). Dla nowych projektów zalecam strict: true od początku. Generujesz plik przez npx tsc --init.

Podstawowy tsconfig.json

{
  "compilerOptions": {
    // Wersja docelowego JavaScript
    "target": "ES2020",

    // Format modułów
    "module": "ESNext",
    "moduleResolution": "node",

    // Tryb ścisły - ZALECANY
    "strict": true,

    // Gdzie zapisywać skompilowane pliki
    "outDir": "./dist",
    "rootDir": "./src",

    // Source maps dla debugowania
    "sourceMap": true,

    // Interoperacyjność z CommonJS
    "esModuleInterop": true,

    // Sprawdzanie bez kompilacji
    "noEmit": false,

    // Dodatkowe sprawdzenia
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Co Robi "strict: true"

Strict mode włącza zestaw sprawdzeń. Możesz je też włączyć pojedynczo:

// strictNullChecks - null i undefined są osobnymi typami
let name: string = "Anna";
// name = null;  // Błąd z strictNullChecks!

let maybeName: string | null = "Anna";
maybeName = null;  // OK

// noImplicitAny - parametry muszą mieć typy
// function greet(name) {}  // Błąd! Parameter 'name' implicitly has 'any' type
function greet(name: string): void {}

// strictFunctionTypes - dokładniejsze sprawdzanie typów funkcji
// strictBindCallApply - sprawdzanie bind, call, apply
// strictPropertyInitialization - właściwości klas muszą być zainicjalizowane

Popularne Konfiguracje

Dla projektu React:

{
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["DOM", "DOM.Iterable", "ESNext"],
    "jsx": "react-jsx",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "noEmit": true,
    "isolatedModules": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}

Dla projektu Node.js:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,
    "sourceMap": true,
    "esModuleInterop": true
  },
  "include": ["src"],
  "exclude": ["node_modules"]
}

11. Moduły i Importy

Organizacja kodu w moduły to podstawa każdego większego projektu. TypeScript w pełni wspiera system modułów ES.

Odpowiedź w 30 sekund

TypeScript używa standardowych modułów ES - export i import. Eksportujesz przez export lub export default, importujesz przez import. Można też re-eksportować z innych modułów. TypeScript dodaje możliwość importowania tylko typów przez import type, co nie generuje kodu runtime.

Podstawowe Eksportowanie

// user.ts

// Named exports
export interface User {
    id: number;
    name: string;
}

export function createUser(name: string): User {
    return { id: Date.now(), name };
}

export const DEFAULT_USER: User = { id: 0, name: "Guest" };

// Default export (jeden na plik)
export default class UserService {
    private users: User[] = [];

    add(user: User): void {
        this.users.push(user);
    }
}

Importowanie

// app.ts

// Import default
import UserService from "./user";

// Import named
import { User, createUser, DEFAULT_USER } from "./user";

// Import all as namespace
import * as UserModule from "./user";
const user: UserModule.User = UserModule.createUser("Anna");

// Import z aliasem
import { User as UserType } from "./user";

// Import tylko typu (nie generuje kodu JS)
import type { User } from "./user";

Re-eksportowanie

// index.ts - barrel file

// Re-eksport wszystkiego
export * from "./user";
export * from "./product";

// Re-eksport z aliasem
export { User as UserModel } from "./user";

// Re-eksport default jako named
export { default as UserService } from "./user";

Import Type vs Import

// Import type - tylko dla typów, usuwane przy kompilacji
import type { User } from "./user";

// Import value - dla rzeczywistych wartości
import { createUser } from "./user";

// Gdy importujesz zarówno typy jak i wartości
import { createUser } from "./user";
import type { User } from "./user";

// Lub w jednej linii (TypeScript 4.5+)
import { createUser, type User } from "./user";

12. Utility Types - Wbudowane Pomocniki Typów

TypeScript dostarcza zestaw wbudowanych typów, które transformują inne typy. Na rozmowach często pytają o najpopularniejsze.

Odpowiedź w 30 sekund

Utility types to generyczne typy wbudowane w TypeScript. Najważniejsze: Partial - wszystkie właściwości opcjonalne, Required - wszystkie wymagane, Readonly - wszystkie tylko do odczytu, Pick<T, K> - wybrane właściwości, Omit<T, K> - wszystkie oprócz wybranych. Oszczędzają dużo ręcznego pisania typów.

Najważniejsze Utility Types

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

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

function updateUser(id: number, updates: Partial<User>): void {
    // Można przekazać dowolny podzbiór właściwości
}
updateUser(1, { name: "Anna" });  // OK

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

// Readonly - wszystkie tylko do odczytu
type ReadonlyUser = Readonly<User>;
const user: ReadonlyUser = { id: 1, name: "Anna", email: "...", age: 25 };
// user.name = "Bartek";  // Błąd!

// Pick - wybrane właściwości
type UserPreview = Pick<User, "id" | "name">;
// { id: number; name: string }

// Omit - wszystkie oprócz wybranych
type UserWithoutEmail = Omit<User, "email">;
// { id: number; name: string; age: number }

Praktyczne Zastosowania

// Record - obiekt z kluczami typu K i wartościami typu V
type UserRoles = Record<string, string[]>;
const roles: UserRoles = {
    admin: ["read", "write", "delete"],
    user: ["read"]
};

// Exclude - usuwa typy z unii
type AllStatuses = "pending" | "success" | "error" | "loading";
type FinalStatuses = Exclude<AllStatuses, "pending" | "loading">;
// "success" | "error"

// Extract - wyciąga pasujące typy z unii
type StringStatuses = Extract<AllStatuses, "success" | "pending">;
// "success" | "pending"

// NonNullable - usuwa null i undefined
type MaybeString = string | null | undefined;
type DefinitelyString = NonNullable<MaybeString>;
// string

// ReturnType - typ zwracany przez funkcję
function createUser(name: string) {
    return { id: Date.now(), name, createdAt: new Date() };
}
type CreatedUser = ReturnType<typeof createUser>;
// { id: number; name: string; createdAt: Date }

// Parameters - typy parametrów funkcji
type CreateUserParams = Parameters<typeof createUser>;
// [name: string]

Łączenie Utility Types

// Kombinacja dla typowego przypadku aktualizacji
interface Article {
    id: number;
    title: string;
    content: string;
    author: string;
    publishedAt: Date;
}

// Do aktualizacji: wszystko opcjonalne oprócz id
type ArticleUpdate = Partial<Omit<Article, "id">> & { id: number };

function updateArticle(update: ArticleUpdate): void {
    // update.id jest wymagane
    // reszta jest opcjonalna
}

updateArticle({ id: 1, title: "New Title" });  // OK
// updateArticle({ title: "New Title" });       // Błąd! id jest wymagane

Na Co Rekruterzy Naprawdę Zwracają Uwagę

Po przeprowadzeniu wielu rozmów rekrutacyjnych na stanowiska wymagające TypeScript, mogę powiedzieć, że sprawdzam następujące rzeczy:

Czy kandydat rozumie "po co", nie tylko "jak". Każdy może zapamiętać składnię interface vs type. Dobry kandydat wyjaśni, kiedy którego użyć i dlaczego. Potrafi podać praktyczne przykłady z własnego doświadczenia.

Czy kandydat pisze typy, które pomagają, a nie przeszkadzają. Zbyt skomplikowane typy mogą być gorsze niż any. Szukam równowagi między bezpieczeństwem a czytelnością. Kandydat, który potrafi uprościć złożony typ, robi dobre wrażenie.

Czy kandydat zna typowe pułapki. Na przykład: readonly nie jest głębokie, object vs {} vs Object to nie to samo, any "zaraża" otaczający kod. Świadomość tych problemów świadczy o praktycznym doświadczeniu.

Czy kandydat korzysta z wnioskowania typów. Nadmierne adnotacje typów to znak początkującego. Profesjonalista pozwala TypeScript wnioskować gdzie może, a dodaje typy tam, gdzie poprawiają czytelność lub są wymagane.

Praktyka na Koniec

Zanim pójdziesz na rozmowę, upewnij się, że potrafisz odpowiedzieć na te pytania:

  1. Napisz generyczną funkcję groupBy<T, K> która grupuje tablicę obiektów po wybranym kluczu.
  2. Jaka jest różnica między object, {} i Object w TypeScript?
  3. Napisz typ DeepReadonly<T> który rekurencyjnie oznacza wszystkie właściwości jako readonly.
  4. Kiedy użyjesz unknown zamiast any i dlaczego?
  5. Jak działa discriminated union i kiedy jest przydatne?
  6. Czym różni się type A = { x: number } & { y: number } od interface A extends X, Y {}?

Jeśli potrafisz na nie odpowiedzieć, masz solidne podstawy TypeScript dla poziomu junior i regular developer.

Zobacz też


Chcesz Więcej Pytań z TypeScript?

Ten artykuł to wprowadzenie do najważniejszych tematów. Mamy ponad 80 pytań z TypeScript z szczegółowymi odpowiedziami, przykładami kodu i wyjaśnieniami - od podstaw przez zaawansowane typy po integrację z React i Node.js.

Sprawdź Pełny Zestaw Pytań TypeScript →

Lub wypróbuj nasz darmowy podgląd pytań, żeby zobaczyć więcej pytań w tym stylu.


Napisane przez zespół Flipcards, na podstawie ponad 15 lat doświadczenia w branży IT i setek przeprowadzonych rozmów rekrutacyjnych w firmach takich jak BNY Mellon, UBS i wiodących firmach fintech.

Chcesz więcej pytań rekrutacyjnych?

To tylko jeden temat z naszego kompletnego przewodnika po rozmowach rekrutacyjnych. Uzyskaj dostęp do 800+ pytań z 13 technologii.

Kup pełny dostęp Zobacz bezpłatny podgląd
Powrót do blogu

Zostaw komentarz

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