Oto pytania z TypeScript, które rekruterzy zadają najczęściej, z krótką odpowiedzią, którą warto umieć podać, i małym przykładem. Są pogrupowane według poziomu; na stanowisko juniorskie zwykle wystarcza pierwsza grupa, a na seniorskie oczekuje się pisania z pamięci typów z ostatniej.
Pytania dla początkujących
1. Czym jest TypeScript?
TypeScript to JavaScript ze statycznymi typami, stworzony przez Microsoft. Oznaczasz wartości typami, kompilator je sprawdza, a potem usuwa typy i generuje zwykły JavaScript, który działa wszędzie tam, gdzie działa JavaScript.
Bez komentarza @ts-expect-error drugie wywołanie jest błędem kompilacji, więc bug nigdy nie trafia na produkcję.
2. Czy TypeScript sprawdza typy w czasie działania?
Nie. Typy są usuwane podczas kompilacji; w wyniku nie zostaje po nich ślad. Powyższy przykład wypisuje 23, bo w czasie działania nic nie wie, że a powinno być liczbą. Żeby sprawdzić dane spoza programu (JSON, dane od użytkownika, odpowiedzi API), piszesz prawdziwe sprawdzenia, na przykład type guard albo walidator schematu.
3. Jakie są podstawowe typy?
string, number (jeden typ dla liczb całkowitych i zmiennoprzecinkowych; nie ma int), boolean, bigint, symbol, null i undefined, a do tego tablice (number[]), krotki ([string, number]), typy obiektowe, any, unknown, never i void. Używaj nazw pisanych małymi literami: String i Number to typy obiektów opakowujących.
4. Czym różni się any od unknown?
Oba przyjmują dowolną wartość. any dodatkowo wyłącza sprawdzanie, więc każda operacja się kompiluje. unknown nie pozwala na nic, dopóki go nie zawęzisz, dlatego jest bezpiecznym typem dla wartości, które nie zostały jeszcze sprawdzone.
5. Czym jest wnioskowanie typów?
Kompilator sam ustala typy, których nie zapisujesz: let count = 0 to number, const mode = "dark" ma typ literałowy "dark", a typ zwracany funkcji wynika z jej instrukcji return. Typowa reguła to oznaczać typy parametrów funkcji i publicznych typów zwracanych, a zmienne lokalne zostawić wnioskowaniu.
6. Czym różni się interface od type?
Oba opisują kształty obiektów, a klasa może robić implements każdego z nich. interface można ponownie otworzyć i połączyć (declaration merging) oraz rozszerzać przez extends inne interfejsy. Alias type może nazwać cokolwiek: unie, krotki, prymitywy, mapped types i conditional types.
interface User { name: string }
interface User { age: number } // merged: User has name and age
type Id = string | number; // only a type alias can be a union
type Pair = [string, number]; // or a tuple
type Admin = User & { role: "admin" };
Częsta konwencja to interface dla kształtów obiektów i type dla całej reszty. Pełne porównanie jest na stronie interface vs type.
7. Czym są typy unii i przecięcia?
Unia A | B to wartość, która jest jednym z tych typów; dopóki jej nie zawęzisz, możesz używać tylko składowych wspólnych dla wszystkich. Przecięcie A & B to wartość, która jest oboma naraz i ma wszystkie składowe każdego z nich.
type Id = string | number; // either
type Timestamped = { createdAt: Date };
type Post = { title: string } & Timestamped; // both: title and createdAt
8. Czym różni się void od never?
void to typ zwracany funkcji, która kończy się normalnie, ale nie zwraca użytecznej wartości. never to typ czegoś, co nie może się wydarzyć: funkcji, która zawsze rzuca wyjątek albo nigdy się nie kończy, albo unii, z której usunięto wszystkie przypadki. never da się przypisać do każdego typu, a do never nie da się przypisać żadnej wartości.
function log(msg: string): void { console.log(msg); }
function fail(msg: string): never { throw new Error(msg); }
type Impossible = string & number; // never
Pytania na poziomie średnim
9. Czym jest zawężanie typów?
Zawężanie (narrowing) to śledzenie twoich sprawdzeń przez kompilator i doprecyzowywanie typu w każdej gałęzi. Kompilator rozumie typeof, instanceof, in, porównania, sprawdzanie prawdziwości i type guards definiowane przez użytkownika.
10. Czym jest type guard definiowany przez użytkownika?
To funkcja, której typ zwracany to value is T. Gdy zwraca true, zmienna wywołującego zostaje zawężona do T. Kompilator ufa tej funkcji, więc jej ciało musi naprawdę sprawdzać wartość.
interface Cat { meow(): void }
function isCat(value: unknown): value is Cat {
return (
typeof value === "object" &&
value !== null &&
"meow" in value &&
typeof value.meow === "function"
);
}
Funkcja asercji, function assertCat(v: unknown): asserts v is Cat, zawęża przez rzucenie wyjątku zamiast zwracania false.
11. Czym są generyki?
To parametry typu, dzięki którym jedna funkcja, klasa albo typ działa z wieloma typami i zachowuje powiązanie między wejściem a wyjściem. T jest wnioskowane z argumentów, a extends ogranicza, czym może być.
pluck(users, "email") byłoby błędem kompilacji, bo "email" nie jest keyof typu użytkownika.
12. Co robią keyof i typeof w typie?
keyof T to unia nazw właściwości T. W pozycji typu typeof x daje typ zmiennej. Razem keyof typeof obj zamienia klucze obiektu w unię.
const colors = { red: "#f00", green: "#0f0" };
type Colors = typeof colors; // { red: string; green: string }
type ColorName = keyof typeof colors; // "red" | "green"
13. Czym są utility types? Wymień kilka.
To wbudowane typy generyczne, które przekształcają inne typy. Najczęściej pojawiają się w pytaniach:
| Utility | Wynik |
|---|---|
Partial<T> | wszystkie właściwości opcjonalne |
Required<T> | wszystkie właściwości wymagane |
Readonly<T> | wszystkie właściwości readonly |
Pick<T, "a" | "b"> | tylko wymienione właściwości |
Omit<T, "a"> | wszystkie właściwości oprócz wymienionych |
Record<K, V> | obiekt z kluczami K i wartościami V |
Exclude<U, X>, Extract<U, X> | usunięcie albo zachowanie elementów unii |
NonNullable<T> | T bez null i undefined |
ReturnType<F>, Parameters<F> | typ zwracany albo typy parametrów funkcji |
Awaited<T> | wartość, na którą rozwiązuje się promise |
Pełna lista jest na stronie o utility types.
14. Czym jest unia dyskryminowana?
To unia typów obiektowych, które mają wspólną literałową właściwość „tag”. Sprawdzenie tagu zawęża wartość do jednego elementu, a sprawdzenie z never w gałęzi default sprawia, że kompilator zgłasza każdy pominięty przypadek.
15. Czym różni się as od satisfies?
value as T to asercja typu: każe kompilatorowi traktować wartość jako T i pomija większość sprawdzeń. value satisfies T sprawdza wartość względem T, ale zachowuje własny, dokładniejszy typ wartości.
type Theme = { primary: string; secondary: string };
const a = { primary: "#07f" } as Theme; // compiles: the missing key is not reported
const b = { primary: "#07f" } satisfies Theme; // error TS2741: Property 'secondary' is missing
const c = {} as { name: string }; // compiles; c.name is undefined at run time
Gdy obiekt jest kompletny, satisfies zachowuje też jego własny wywnioskowany typ: sprawdzona względem Record<string, string> zmienna nadal dokładnie wie, jakie ma klucze, a adnotacja poszerzyłaby ją do dowolnego klucza typu string.
16. Co robi operator ! po zmiennej?
To asercja non-null: el! usuwa null i undefined z typu. Nie generuje żadnego sprawdzenia, więc jeśli wartość faktycznie jest null, program wysypie się w miejscu użycia. Lepiej użyć ?., ?? albo jawnego if.
17. Czym różni się private od #private?
private egzekwuje tylko kompilator; w wynikowym JavaScripcie to zwykła właściwość. #field to prywatne pole JavaScriptu, egzekwowane przez środowisko uruchomieniowe.
protected działa jak private, ale pozwala też na dostęp z podklas, a readonly zabrania ponownego przypisania po konstrukcji.
18. Czym różni się klasa abstrakcyjna od interfejsu?
Interfejs to tylko typ: opisuje kształt i znika z wyniku. Klasa abstrakcyjna to prawdziwa klasa, której nie można zainstancjonować; może zawierać zaimplementowane metody, pola i konstruktory obok składowych abstract, które podklasy muszą zaimplementować. Klasa może implementować wiele interfejsów, ale rozszerzać tylko jedną klasę.
abstract class Repository<T> {
protected items: T[] = [];
abstract validate(item: T): boolean; // subclasses must implement
add(item: T): void { // shared implementation
if (this.validate(item)) this.items.push(item);
}
}
19. Czym jest przeciążanie funkcji w TypeScript?
To kilka sygnatur wywołania, po których następuje jedna implementacja z sygnaturą zgodną z nimi wszystkimi. Wywołujący widzą tylko przeciążenia, więc każde wywołanie dostaje dokładny typ zwracany.
function parse(value: string): number;
function parse(value: string[]): number[];
function parse(value: string | string[]): number | number[] {
return Array.isArray(value) ? value.map(Number) : Number(value);
}
const one = parse("4"); // number
const many = parse(["1", "2"]); // number[]
Gdy typ zwracany nie zależy od typu argumentu, prostsza jest jedna sygnatura z parametrem typu unii.
Pytania zaawansowane
20. Czym jest typowanie strukturalne?
TypeScript porównuje typy według kształtu, a nie nazwy. Akceptowana jest każda wartość z wymaganymi właściwościami, nawet jeśli nigdy nie została zadeklarowana z tym typem.
Dodatkowe color nie przeszkadza, bo pixel jest zmienną. Przekazanie literału obiektowego bezpośrednio, show({ x: 3, y: 4, color: "red" }), to błąd nadmiarowej właściwości, czyli sprawdzenie, które dotyczy tylko świeżych literałów. Gdy dwa typy o tym samym kształcie nie powinny się mieszać (UserId i OrderId, które oba są stringami), użyj branded type: type UserId = string & { readonly __brand: "UserId" }.
21. Jak samodzielnie zaimplementować Readonly i Partial?
Przez mapped types: przejdź po keyof T i dodaj modyfikator do każdej właściwości.
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };
type MyPartial<T> = { [K in keyof T]?: T[K] };
type Mutable<T> = { -readonly [K in keyof T]: T[K] }; // "-" removes a modifier
type User = { name: string; age: number };
type Draft = MyPartial<User>; // { name?: string; age?: number }
22. Czym są conditional types i infer?
T extends U ? X : Y wybiera typ na podstawie warunku. Wewnątrz warunku infer deklaruje zmienną typu, która przechwytuje część dopasowanego typu. Conditional types rozkładają się na elementy unii.
type MyReturnType<F> = F extends (...args: any[]) => infer R ? R : never;
type ElementOf<T> = T extends (infer E)[] ? E : T;
type A = MyReturnType<() => Promise<number>>; // Promise<number>
type B = ElementOf<string[]>; // string
type C = ElementOf<number | boolean[]>; // number | boolean (distributed)
Więcej wzorców jest na stronie o conditional types.
23. Czym jest plik .d.ts i co robi declare?
Plik deklaracji zawiera tylko typy dla kodu, który istnieje gdzie indziej, na przykład biblioteki JavaScript albo API przeglądarki. declare stwierdza, że wartość istnieje, bez jej tworzenia: declare const VERSION: string; kompiluje się do niczego. Typy bibliotek są dołączone do pakietu albo pochodzą z pakietów @types/{name}, a tsc generuje pliki .d.ts dla twojego kodu przy declaration: true.
24. Co robi strict i które opcje tsconfig są najważniejsze?
"strict": true włącza noImplicitAny, strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, useUnknownInCatchVariables i strictBuiltinIteratorReturn. W TypeScript 7 jest włączone domyślnie. Inne opcje, o które pytają rekruterzy:
| Opcja | Odpowiedź w jednej linii |
|---|---|
target | której wersji JavaScriptu używa wynik |
module | format modułów w wyniku: nodenext stosuje reguły Node, esnext/preserve zostawiają import dla bundlera |
moduleResolution | jak znajdowane są importy: nodenext wymaga ./file.js w plikach ES module, bundler akceptuje ./file |
noUncheckedIndexedAccess | arr[i] ma w typie także undefined |
paths | aliasy importów tylko dla sprawdzania typów; wynik zachowuje alias, więc musi go rozwiązać bundler albo pole imports w Node |
skipLibCheck | pomija sprawdzanie plików .d.ts, dla szybkości |
verbatimModuleSyntax | importy używane tylko jako typy muszą być oznaczone type; pozostałe importy zostają w zapisanej postaci |
25. Czym jest TypeScript 7?
To kompilator TypeScript przepisany w Go jako natywny program, wydany jako wersja 7 pakietu npm typescript. Polecenie to nadal tsc, a język jest ten sam, ale działa znacznie szybciej niż oparty na JavaScripcie kompilator 6.x: zespół TypeScript podaje przyspieszenie od 8x do 12x przy typowych pełnych buildach, dzięki natywnemu kodowi i wielowątkowości ze współdzieloną pamięcią. Egzekwuje też usunięcia, które 6.0 oznaczyło jako przestarzałe; na przykład baseUrl, outFile, moduleResolution: "node10", module: "amd" i alwaysStrict: false są teraz błędami (TS5102, TS5108), które każą je usunąć.
Najczęściej zadawane pytania
Jakie pytania z TypeScript padają na rozmowach najczęściej?
Najczęściej: różnica między any a unknown, interface kontra type, jak działają generyki, jak działa zawężanie i type guards, co robią utility types, takie jak Partial, Pick i Omit, oraz czy TypeScript sprawdza typy w czasie działania (nie sprawdza).
Jakie pytania z TypeScript zadaje się seniorom?
Spodziewaj się pisania typów, a nie tylko ich czytania: implementacji Readonly albo ReturnType przez mapped i conditional types, wyjaśnienia typowania strukturalnego i tego, kiedy używać branded types, zaprojektowania unii dyskryminowanej z exhaustive check oraz rozmowy o opcjach tsconfig, takich jak strict, noUncheckedIndexedAccess i rozwiązywanie modułów.
Jak przygotować się do rozmowy rekrutacyjnej z TypeScript?
Pisz małe programy z włączonym strict i czytaj błędy kompilatora, aż zaczniesz je przewidywać. Musisz umieć wyjaśnić zawężanie, generyki z ograniczeniami, główne utility types i to, dlaczego asercje typów są niebezpieczne, każdą rzecz z przykładem na dwie linie.
Czy na rozmowach z React i Angular pytają o TypeScript?
Zwykle tak. Angular jest napisany w TypeScript i aplikacje w nim też, więc rozmowy o Angularze zwykle zakładają jego znajomość, łącznie z dekoratorami, takimi jak @Component, i modyfikatorami dostępu. Rozmowy o React w projekcie z TypeScript często wymagają otypowania propsów, stanu i handlerów zdarzeń, co korzysta z tych samych interfejsów, unii i generyków, które widać tutaj.