{ts}
Leveling Up TypeScript
Mapped Types
Conditional Types
Utility Types
Distributive Conditional Types
Generics
Template Literal Types
Variadic Tuple Types
Indexed Access Types
Discriminated Unions
Recursive Types
Bounded Polymorphism
Nominal Types
Type Guards
Type Predicates
Advanced TypeScript
typeof
keyof
infer
// Use template literals to prevent typos in endpoint strings
type ApiPaths = `/api/v1/${
| 'account/activity'
| 'accounts/summary'
| 'billing/payments'}`;
# LEVEL UP TYPESCRIPT
Template Literal Types
const baseUrl = 'https://my-api.com';
const paths: Record<string, ApiPaths> = {
activity: '/api/v1/account/activity',
summary: '/api/v1/accounts/summary',
payments: '/api/v1/billing/payments'
};
const isValidEndpoint = (url: URL) => {
return Object.values(paths).includes(url.pathname as ApiPaths);
};
const createEndpoint = (path: ApiPaths) => {
const url = new URL(path, baseUrl);
if (!isValidEndpoint(url) throw Error('Invalid endpoint');
return url;
};
# LEVEL UP TYPESCRIPT
Type Guards
const baseUrl = 'https://my-api.com';
const paths: Record<string, ApiPaths> = {
activity: '/api/v1/account/activity',
summary: '/api/v1/accounts/summary',
payments: '/api/v1/billing/payments'
};
type ValidEndpoint<T extends ApiPaths = ApiPaths> = URL & { pathname: T };
const isValidEndpoint = (url: URL): url is ValidEndpoint => {
return Object.values(paths).includes(url.pathname as ApiPaths);
};
const createEndpoint = <T extends ApiPaths>(path: T): ValidEndpoint<T> => {
const url = new URL(path, baseUrl);
if (!isValidEndpoint(url)) throw Error('Invalid endpoint');
return url as ValidEndpoint<T>;
};
# LEVEL UP TYPESCRIPT
Type Guards & Type Predicates
// const isValidEndpoint = (url: URL): url is ValidEndpoint => {
// return Object.values(paths).includes(url.pathname as ApiPaths);
// };
const isValidEndpoint = <T extends string>(url: URL, paths: T[]):
url is URL & { pathname: T } => {
return paths.includes(url.pathname as T);
};
const createEndpoint = (path: ApiPaths) => {
const url = new URL(path, baseUrl);
if (!isValidEndpoint(url, Object.values(paths)) {
throw Error('Invalid endpoint');
}
return url;
};
# LEVEL UP TYPESCRIPT
Generic Type Guards
type MailingAddress = {
street1: string;
street2: string;
city: string;
state: string;
zipCode: string;
};
# LEVEL UP TYPESCRIPT
Mapped Types
type MailingAddress = {
street1: string;
street2: string;
city: string;
state: string;
zipCode: string;
};
type MailingAddressForm = {
street1: boolean;
street2: boolean;
city: boolean;
state: boolean;
zipCode: boolean;
};
# LEVEL UP TYPESCRIPT
Mapped Types
type MailingAddress = {
street1: string;
street2: string;
city: string;
state: string;
zipCode: string;
};
type MailingAddressForm = {
street1?: boolean;
street2?: boolean;
city?: boolean;
state?: boolean;
zipCode?: boolean;
};
# LEVEL UP TYPESCRIPT
Mapped Types
type MailingAddress = {
street1: string;
street2: string;
city: string;
state: string;
zipCode: string;
};
type MailingAddressForm = {
[K in keyof MailingAddress]?: boolean;
};
# LEVEL UP TYPESCRIPT
Mapped Types
type MailingAddress = {
street1: string;
street2: string;
city: string;
state: string;
zipCode: string;
};
type MailingAddressForm = {
[K in keyof MailingAddress]?: boolean;
};
type MailingAddressPayload = {
readonly [K in keyof MailingAddress as `cipher.${K}`]: MailingAddress[K]
};
# LEVEL UP TYPESCRIPT
Mapped Types
type RequestBodyMapping = {
[paymentsPath]: { payment: number };
};
// Use conditional types to enforce the correct structure
type RequestBody<T> = T extends keyof RequestBodyMapping
? RequestBodyMapping[T]
: never;
# LEVEL UP TYPESCRIPT
Conditional Types
type AccountActivity = { activity: [] };
type Account = { accountId: string, address: MailingAddressPayload };
type AccountSummary = { accounts: Account[] };
type Payment = { paymentId: string, paymentAmount: number };
type RequestMapping = {
[activityPath]: AccountActivity,
[summaryPath]: AccountSummary,
[paymentsPath]: Payment
};
// Indexed access type to extract response type from mapping
type ExtractResponse<T> = T extends keyof RequestMapping
? RequestMapping[T]
: never;
# LEVEL UP TYPESCRIPT
Indexed Access Types
type AccountActivity = { activity: [] };
type Account = { accountId: string, address: MailingAddressPayload };
type AccountSummary = { accounts: Account[] };
type Payment = { paymentId: string, paymentAmount: number };
type RequestMapping = {
[activityPath]: AccountActivity,
[summaryPath]: AccountSummary,
[paymentsPath]: Payment
};
// Indexed access type to extract response type from mapping
// type ExtractResponse<T> = T extends keyof RequestMapping
// ? RequestMapping[T]
// : never;
// Prevent automatic union distribution with single-element tuple
type ExtractResponse<T> = [T] extends [keyof RequestMapping]
? RequestMapping[T]
: never;
# LEVEL UP TYPESCRIPT
Indexed Access Types
type RequestFn = <T extends keyof RequestMapping>(
url: ValidEndpoint,
method?: HttpMethod,
body?: RequestBody<T>
) => Promise<ExtractResponse<T>>;
// Utility types to unwrap the returned Promise type and extract the data
type ResponseData<T extends keyof RequestMapping> =
Extract<Awaited<ReturnType<RequestFn<T>>>, { status: 'success' }>['data'];
# LEVEL UP TYPESCRIPT
Utility Types
// Discriminated union narrows type for handling both cases
type ApiResponse<T> =
| { status: 'success', data: T }
| { status: 'error', error: string };
# LEVEL UP TYPESCRIPT
Discriminated Unions
// Discriminated union narrows type for handling both cases
type ApiResponse<T> =
| { status: 'success', data: T }
| { status: 'error', error: string };
const request = async <T extends keyof RequestMapping>(
url: ValidEndpoint, method = 'GET', body?: RequestBody<T>):
Promise<ApiResponse<ExtractResponse<T>>> => {
const response = await fetch(url, { method, body: JSON.stringify(body) });
if (!response.ok) {
return {
status: 'error',
error: `Request failed with ${response.status}`
};
}
const data: ExtractResponse<T> = await response.json();
return { status: 'success', data };
};
# LEVEL UP TYPESCRIPT
Discriminated Unions
// Variadic tuple type allows multiple requests while preserving exact types
type BatchRequestTuple<T extends keyof RequestMapping, M extends HttpMethod> =
M extends 'POST'
? readonly [T, M, RequestBody<T>]
: readonly [T, M];
type BatchRequestList = readonly BatchRequestTuple<keyof RequestMapping,
HttpMethod>[];
type BatchRequestFn = <T extends BatchRequestList>(...requests: T) =>
Promise<BatchRequestResponse<T>>;
type BatchRequestResponse<T extends BatchRequestList> = {
[K in keyof T]:
Extract<ApiResponse<ExtractResponse<T[K][0]>>, { status: 'success' }>;
};
# LEVEL UP TYPESCRIPT
Variadic Tuple Types
const batchRequest: BatchRequestFn = async (...requests) => {
const responses = await Promise.all(
requests.map(([url, method, body]) =>
request(createEndpoint(url) as ValidEndpoint<typeof url>, method, body)
)
);
return responses.map((res) => {
if (res.status !== 'success') throw new Error(res.error);
return res;
}) as BatchRequestResponse<typeof requests>;
};
(async () => {
const [summaryRes, paymentRes] = await batchRequest(
[summaryPath, 'GET'],
[paymentsPath, 'POST', { payment: 100 }]
);
console.log(summaryRes.data.accounts, paymentRes.data.paymentId);
})();
# LEVEL UP TYPESCRIPT
Variadic Tuple Types
DEMO
Balancing Advanced Types
1. Type Safety vs. Complexity
- Aim for type safety that prevents real-world bugs without making the types overly complex.
- Use mapped types, conditional types, and indexed access types where they add value, but avoid unnecessary abstractions.
Balancing Advanced Types
2. Readability and Developer Experience
- Types should be understandable at a glance — future developers (including yourself) should not struggle to interpret them.
- Use clear naming conventions (
ExtractResponse<T>
,BatchRequestTuple<T>
) and avoid deeply nested types unless truly necessary.
Balancing Advanced Types
3. Scalability and Reusability
- Generic utility types (
BatchRequestFn<T>
) can make code more scalable but should be designed with reusability in mind.
- Consider trade-offs: Will this type be useful across multiple parts of the codebase, or is it overly specific?
Final Thought
The best TypeScript code is not the one with the most advanced types, but the one that strikes the right balance — providing strong guarantees without making the codebase harder to work with.
Final Thought
The best TypeScript code is not the one with the most advanced types, but the one that strikes the right balance — providing strong guarantees without making the codebase harder to work with.

Leveling Up TypeScript
By webguyian
Leveling Up TypeScript
Leveling Up TypeScript
- 154