{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.

Made with Slides.com