For the complete documentation index, see llms.txt. This page is also available as Markdown.

TypeScript guidelines

Start from the beginning

The best way to approach typing in an application is always from the foundations, defining the types of your data at the first moment it appears in your code.

  • In a backend application that queries a database, start by typing your database models.

  • In a frontend application that queries an API, start by typing the API responses.

You can save a lot of work by adding libraries to your stack that generate types automatically (TypeScript ORMs, API clients, Swagger/OpenAPI codegen, GraphQL codegen...). We recommend Hey API (@hey-api/openapi-ts) to generate a fully typed client from an OpenAPI specification, and Drizzle as the TypeScript ORM, defining your database schema in TypeScript and inferring the types of your models from it.

Don't repeat yourself

Never repeat types, use and abuse generics and utility types to derive them.

Some examples:

// This auxiliary type defines the return type of our REST client that responds with a type to be defined for each request
export type APIRequest<T> = Promise<{ data: T; statusCode: number }>;

// Each different user role
export enum UserRole {
  ADMIN = "admin",
  USER = "user",
}

// User interface as returned from the backend
export type User = {
  email: string;
  name: string;
  phone: number;
  role: UserRole;
  productIds?: number[];
};

// So, our user request response will be
export type UserRequestResponse = APIRequest<User>;

// Suppose the user creation form UI doesn't allow defining every User field, so we pick only what we need using Pick
// Also, for demonstration purposes, imagine that the form is filled in different steps and is not fully completed from the beginning,
// so we can mark every field as optional (?) using Partial
export type CreateUserFormData = Partial<Pick<User, "email" | "name" | "phone">>;

// But for the method that will send the request, those fields are required, so we can remove every optional field (?) with Required
export type CreateUserPayload = Required<CreateUserFormData>;

// When obtaining the new user response, it could be useful to populate the user products with the full product objects obtained
// from other requests, so the user returned from the method once populated could be:
export type PopulatedUser = Omit<User, "productIds"> & { products: Product[] };

// In a React application, suppose a card component that exposes User information, its props could be
export type UserCardProps = {
  user: PopulatedUser
}
export function UserCard({ user }: UserCardProps) { ... }

Prefer types over interfaces

Use type instead of interface for defining object shapes and other type aliases. type is more versatile and consistent across different use cases.

Advantages of type:

  • Can represent unions, intersections, tuples, mapped types, and conditional types — interface cannot

  • More consistent: one syntax for all type definitions

  • Cannot be declaration-merged (prevents accidental augmentation from other files)

  • Works more naturally with utility types (Pick, Omit, Partial, etc.)

What type can do that interface cannot:

Declaration merging pitfall:

Exception: interface is acceptable when you intentionally need declaration merging (e.g., augmenting third-party library types or extending Window).

Use explicit return types for complex functions

Functions returning complex types that aren't easily inferred must have explicit return type annotations. Simple functions with obvious inference don't need them.

Why explicit return types matter:

  • Acts as documentation of the function contract

  • Catches implementation errors at the function boundary, not at distant call sites

  • Prevents accidental return type changes from silently propagating

  • Produces better, more localized error messages

Problems when omitted:

  • A small implementation change can accidentally alter the inferred return type, breaking callers far away

  • Error messages appear at call sites instead of at the function definition

  • Harder to understand what a function returns without reading its full implementation

Accidental return type change without explicit annotation:

When explicit return types aren't needed:

Use named types, avoid anonymous types

Prefer named types over inline/anonymous type literals. Named types improve reusability, readability, and maintainability.

Advantages:

  • Reusable across the codebase

  • Better error messages — TypeScript shows the type name instead of the full expanded structure

  • Self-documenting: a name communicates intent

  • Easier refactoring — change the type definition in one place

  • Better IDE experience (hover tooltips show meaningful names)

Problems with anonymous types:

  • Cannot be reused, leading to duplication

  • Error messages show the full object structure, making them hard to read

  • No single source of truth — changes must be made in every occurrence

Anonymous types causing duplication and poor readability:

Anonymous types in React components:

See also the component conventions in our React guidelines.

Export every type

In an ideal world, all libraries would export the types of the objects they provide access to. Unfortunately, this is not always the case, so export every type you create. If you find yourself working with types that you don't have direct access to, create your derivatives as soon as possible. Unwrapping an inaccessible type can be too complex and is often a verbose and difficult-to-read operation. If all types were exported, we could avoid things like:

Instead of the above, with exported types it would be simpler:

Avoid casting when possible

Excessive use of casting (as Type, <Type>value) often indicates problems in the design of types or the structure of the code. If you find yourself using many casts, you may be fighting against the type system rather than leveraging it.

Casts create blind spots in the type system, as you're telling the compiler to "trust you" rather than properly verifying types. This can lead to runtime errors that are difficult to detect.

Alternatives to casting

  1. Type Guards: Functions that help TypeScript recognize types at runtime.

  1. Assertion Functions: Functions that throw an error if the condition is not met.

  1. Validation Schemas: Use libraries like Zod, io-ts, or Ajv to validate and type data simultaneously.

The goal should always be to create code that is type-safe by design, rather than forcing types with casting.

Don't carry nullable values in function parameters

An important principle in type design is to avoid "carrying" nullable values (null or undefined) through the function chain. If a parameter can be nullable, it's better to handle it as early as possible in your code.

When you allow nullable values to propagate through multiple functions, each function needs to check if the value is nullable, which causes:

  1. Code repetition (each function repeats the same checks)

  2. Increased complexity (code full of conditional checks)

  3. Higher probability of errors (if a check is forgotten)

  4. Less readable and harder to maintain code

Instead, it's better to validate nullable values as early as possible and work only with non-nullable values:

Benefits of early nullable handling

  1. Simpler functions: Each function has a clear purpose and doesn't worry about nullable values.

  2. Better type inference: TypeScript can infer more precise types, reducing the need for type annotations.

  3. Safer code: Less likelihood of TypeError: Cannot read property 'x' of null errors.

  4. Better testability: Functions with non-nullable inputs are easier to test.

Techniques for handling nullable values

  1. Early validation:

  1. Default values:

  1. Pattern matching / discriminated unions:

This approach of handling nullable values early in the application flow and working with non-nullable types in most of the code leads to a more robust and easier to maintain system.

Nullables in React functional components

This principle is especially important in React functional components. React components often receive props that may be nullable, and it's easy to fall into patterns where these checks are repeated in multiple components or in multiple parts of the same component.

Better approach:

  1. Validation in the main component:

  1. Using nullish coalescing operators and default values in props:

Last updated