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
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
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
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
// 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
type MailingAddress = {
street1: string;
street2: string;
city: string;
state: string;
zipCode: string;
};
# LEVEL UP TYPESCRIPT
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
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
type MailingAddress = {
street1: string;
street2: string;
city: string;
state: string;
zipCode: string;
};
type MailingAddressForm = {
[K in keyof MailingAddress]?: boolean;
};
# LEVEL UP TYPESCRIPT
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
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
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
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
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
// Discriminated union narrows type for handling both cases
type ApiResponse<T> =
| { status: 'success', data: T }
| { status: 'error', error: string };
# LEVEL UP TYPESCRIPT
// 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
// 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
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
1. Type Safety vs. Complexity
2. Readability and Developer Experience
ExtractResponse<T>
, BatchRequestTuple<T>
) and avoid deeply nested types unless truly necessary.3. Scalability and Reusability
BatchRequestFn<T>
) can make code more scalable but should be designed with reusability in mind.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.
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.