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:
- Generics - parametry typów, constraints, wielokrotne parametry
-
Conditional types -
extends, distributive,infer - Mapped types - transformacje, key remapping, modyfikatory
- Template literal types - konkatenacja, parsowanie, intrinsic types
- Utility types - wbudowane i własne
- Type guards - narrowing, custom guards, assertions
- 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.