TypeScript 5.5 Advanced Features: Type Safety na sterydach
TypeScript 5.5 Advanced Features: Type Safety na sterydach
TypeScript przestał być "nice to have" - w 2025 roku to industry standard. 87% nowych projektów JavaScript używa TypeScript (Stack Overflow Survey 2025), a firmy jak Airbnb, Slack i Shopify przepisały miliony linii kodu z JavaScript na TS.
Dlaczego? Jeden prosty powód: błędy wykrywane w compile-time, nie w production.
W tym przewodniku pokażę Ci zaawansowane features TypeScript 5.5, których większość developerów nie używa - a powinni. Template literal types, const assertions, branded types, discriminated unions i wiele więcej.
Spis treści
- Template Literal Types - dynamiczne typy ze stringów
- Const Assertions - immutability bez 'as const'
- Branded Types - runtime safety w compile time
- Discriminated Unions - type-safe state machines
- Conditional Types - if/else dla typów
- infer keyword - type extraction magic
- Mapped Types - DRY principle dla typów
1. Template Literal Types - Dynamiczne typy ze stringów
Problem: Jak stypować CSS classes?
// ❌ ZŁE: Any string allowed
function applyClass(className: string) {
element.classList.add(className);
}
applyClass('bg-red-500'); // ✅ OK
applyClass('bg-red-5000'); // ❌ Typo, but no error!
✅ Solution: Template Literal Types
type Color = 'red' | 'blue' | 'green' | 'yellow';
type Shade = '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900';
type TailwindColor = `bg-${Color}-${Shade}`;
function applyClass(className: TailwindColor) {
element.classList.add(className);
}
applyClass('bg-red-500'); // ✅ OK
applyClass('bg-red-5000'); // ❌ Compile error!
Result: TypeScript autocomplete pokazuje wszystkie 36 możliwych kombinacji! 🎯
Real-world Example: API Route Types
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type APIVersion = 'v1' | 'v2';
type Resource = 'users' | 'posts' | 'comments';
type APIRoute = `/${APIVersion}/${Resource}`;
// Result: "/v1/users" | "/v1/posts" | "/v1/comments" | "/v2/users" | ...
type APIEndpoint = `${HTTPMethod} ${APIRoute}`;
// Result: "GET /v1/users" | "POST /v1/users" | "DELETE /v2/comments" | ...
function callAPI(endpoint: APIEndpoint) {
const [method, route] = endpoint.split(' ');
// TypeScript knows method is HTTPMethod and route is APIRoute!
}
callAPI('GET /v1/users'); // ✅ OK
callAPI('PATCH /v1/users'); // ❌ Error: PATCH nie jest w HTTPMethod
2. Const Assertions - Immutability bez 'as const'
Problem: Mutable types by default
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3,
};
// TypeScript infers:
// {
// apiUrl: string; ← Too broad!
// timeout: number; ← Too broad!
// retries: number; ← Too broad!
// }
config.apiUrl = 'https://hack.me'; // ❌ No error, but dangerous!
✅ Solution: as const
assertion
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3,
} as const;
// TypeScript infers:
// {
// readonly apiUrl: "https://api.example.com"; ← Exact type!
// readonly timeout: 5000; ← Exact type!
// readonly retries: 3; ← Exact type!
// }
config.apiUrl = 'https://hack.me'; // ✅ Compile error: cannot assign to readonly!
Advanced: Const assertion dla arrays
// ❌ ZŁE: Mutable array
const colors = ['red', 'blue', 'green'];
// Type: string[] ← Any string can be added/removed
colors.push('purple'); // No error
colors[0] = 'yellow'; // No error
// ✅ DOBRE: Immutable tuple
const colors = ['red', 'blue', 'green'] as const;
// Type: readonly ["red", "blue", "green"] ← Exact, immutable tuple
colors.push('purple'); // ❌ Error: push doesn't exist on readonly array
colors[0] = 'yellow'; // ❌ Error: cannot assign to readonly
Real-world: Type-safe enum alternative
// Instead of enum (which compiles to JS)
enum Status {
Pending,
Active,
Completed
}
// Use const assertion (zero JS output!)
const Status = {
Pending: 'pending',
Active: 'active',
Completed: 'completed',
} as const;
type Status = typeof Status[keyof typeof Status];
// Type: "pending" | "active" | "completed"
function updateStatus(status: Status) {
// TypeScript autocomplete works perfectly!
}
updateStatus(Status.Active); // ✅ OK
updateStatus('active'); // ✅ OK (string literal)
updateStatus('invalid'); // ❌ Error
3. Branded Types - Runtime Safety w Compile Time
Problem: Structural typing może być za luźne
type UserId = string;
type ProductId = string;
function getUser(id: UserId) { /* ... */ }
function getProduct(id: ProductId) { /* ... */ }
const userId: UserId = 'user_123';
const productId: ProductId = 'prod_456';
getUser(productId); // ❌ Should error, but doesn't! (both are strings)
✅ Solution: Branded Types (Nominal Typing)
type Brand<K, T> = K & { __brand: T };
type UserId = Brand<string, 'UserId'>;
type ProductId = Brand<string, 'ProductId'>;
function getUser(id: UserId) { /* ... */ }
function getProduct(id: ProductId) { /* ... */ }
// Smart constructor
function createUserId(id: string): UserId {
if (!id.startsWith('user_')) throw new Error('Invalid UserId');
return id as UserId;
}
const userId = createUserId('user_123');
const productId = 'prod_456' as ProductId;
getUser(userId); // ✅ OK
getUser(productId); // ❌ Compile error! ProductId !== UserId
Advanced: Email, URL, NonEmptyString validation
type Email = Brand<string, 'Email'>;
type URL = Brand<string, 'URL'>;
type NonEmptyString = Brand<string, 'NonEmptyString'>;
function createEmail(value: string): Email {
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
throw new Error('Invalid email');
}
return value as Email;
}
function createURL(value: string): URL {
try {
new URL(value);
return value as URL;
} catch {
throw new Error('Invalid URL');
}
}
function createNonEmptyString(value: string): NonEmptyString {
if (value.trim().length === 0) {
throw new Error('String cannot be empty');
}
return value as NonEmptyString;
}
// Usage
function sendEmail(to: Email, body: NonEmptyString) {
// TypeScript guarantees 'to' is valid email and 'body' is not empty!
}
sendEmail(
createEmail('user@example.com'),
createNonEmptyString('Hello!')
); // ✅ OK
sendEmail('invalid-email', ''); // ❌ Compile errors on both args!
4. Discriminated Unions - Type-safe State Machines
Problem: Union types bez discriminatora
type Response = {
loading: boolean;
data?: User;
error?: string;
};
function handleResponse(response: Response) {
if (response.loading) {
// ⚠️ TypeScript doesn't know data/error are undefined here
console.log(response.data?.name); // Might be undefined!
}
}
✅ Solution: Discriminated Union
type Response =
| { status: 'loading' }
| { status: 'success'; data: User }
| { status: 'error'; error: string };
function handleResponse(response: Response) {
switch (response.status) {
case 'loading':
// TypeScript knows: only 'status' exists here
console.log('Loading...');
break;
case 'success':
// TypeScript knows: 'data' exists and 'error' doesn't
console.log(response.data.name); // ✅ No optional chaining needed!
break;
case 'error':
// TypeScript knows: 'error' exists and 'data' doesn't
console.log(response.error); // ✅ Safe!
break;
}
}
Real-world: Form validation state
type FormState =
| { status: 'idle' }
| { status: 'validating' }
| { status: 'valid'; values: FormValues }
| { status: 'invalid'; errors: Record<string, string> }
| { status: 'submitting'; values: FormValues }
| { status: 'submitted'; result: SubmitResult };
function FormComponent({ state }: { state: FormState }) {
switch (state.status) {
case 'idle':
return <button>Start</button>;
case 'validating':
return <Spinner />;
case 'valid':
return <button onClick={() => submit(state.values)}>Submit</button>;
case 'invalid':
return <ErrorList errors={state.errors} />;
case 'submitting':
return <button disabled>Submitting...</button>;
case 'submitted':
return <Success result={state.result} />;
}
}
Benefit: Impossible states are impossible! Nie możesz mieć loading: true
i data: User
jednocześnie.
5. Conditional Types - If/Else dla typów
Basic Syntax
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
Real-world: Extract function return types
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function getUser() {
return { id: 1, name: 'John' };
}
type User = ReturnType<typeof getUser>;
// Type: { id: number; name: string; }
Advanced: Flatten array types
type Flatten<T> = T extends Array<infer U> ? U : T;
type A = Flatten<string[]>; // string
type B = Flatten<number[]>; // number
type C = Flatten<string>; // string (not array)
Utility: DeepPartial (recursive)
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
interface User {
id: number;
profile: {
name: string;
address: {
street: string;
city: string;
};
};
}
type PartialUser = DeepPartial<User>;
// All nested properties are optional!
const user: PartialUser = {
profile: {
address: {
city: 'NYC' // Only city, all others optional!
}
}
};
6. infer keyword - Type Extraction Magic
Extract Promise resolved type
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type A = UnwrapPromise<Promise<string>>; // string
type B = UnwrapPromise<Promise<number>>; // number
type C = UnwrapPromise<boolean>; // boolean
Extract function parameters
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
function example(a: string, b: number, c: boolean) {}
type Params = Parameters<typeof example>;
// Type: [string, number, boolean]
Extract React component props
type ComponentProps<T> = T extends React.FC<infer P> ? P : never;
const Button: React.FC<{ label: string; onClick: () => void }> = ({ label, onClick }) => (
<button onClick={onClick}>{label}</button>
);
type ButtonProps = ComponentProps<typeof Button>;
// Type: { label: string; onClick: () => void }
7. Mapped Types - DRY Principle dla typów
Utility: Make all properties required
type Required<T> = {
[P in keyof T]-?: T[P];
};
interface User {
id: number;
name?: string;
email?: string;
}
type RequiredUser = Required<User>;
// Type: { id: number; name: string; email: string; } ← All required!
Utility: Make all properties readonly
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type ReadonlyUser = Readonly<User>;
// All properties are readonly
Advanced: Getters type from object
type Getters<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};
interface State {
count: number;
name: string;
}
type StateGetters = Getters<State>;
// Type: {
// getCount: () => number;
// getName: () => string;
// }
Podsumowanie: TypeScript Best Practices 2025
✅ DO:
- Template literal types dla string unions
- Const assertions dla config objects
- Branded types dla critical data (IDs, emails)
- Discriminated unions dla state machines
- Conditional types dla utility types
- infer do type extraction
- Mapped types dla transformations
❌ DON'T:
any
type (useunknown
instead)- Type assertions (
as
) bez validacji - Enums (use const objects +
as const
) @ts-ignore
(fix the root cause!)- Explicit return types gdy TypeScript infers correctly
🎯 ROI:
- 40% mniej runtime errors (Microsoft Research)
- 15% szybszy code review (less mental overhead)
- Better refactoring (rename/extract with confidence)
Powiązane artykuły
Potrzebujesz pomocy z TypeScript w projekcie? Skontaktuj się ze mną - migruję legacy JS codebases do type-safe TypeScript!
Autor: Next Gen Code | Data publikacji: 18 października 2025 | Czas czytania: 11 minut
Powiązane artykuły
AI w Web Development 2025: Jak automatyzować kodowanie bez utraty bezpieczeństwa
# AI w Web Development 2025: Jak automatyzować kodowanie bez utraty bezpieczeństwa Sztuczna inteligencja **radykalnie zmienia sposób tworzenia aplikacji webow...
NextGenScan: Sekret za 38% szybszym wykrywaniem zagrożeń w Twojej aplikacji
**Czy kiedykolwiek zastanawiałeś się, co naprawdę dzieje się pod maską Twojej aplikacji?** W ciemnych zakamarkach kodu, gdzie kończy się to, co widzisz w prze...
React Server Components w praktyce: Kompletny przewodnik 2025
# React Server Components w praktyce: Kompletny przewodnik 2025 React Server Components (RSC) to **rewolucja w architekturze aplikacji webowych**, która funda...