Skip to main content
Trezor Suite leverages TypeScript for type safety and better developer experience. Follow these conventions to write maintainable, type-safe code.

Type Safety Best Practices

Prefer @ts-expect-error to @ts-ignore

@ts-ignore suppresses errors silently, making it dangerous when errors are fixed or new ones appear.
TypeScript 3.9+ provides @ts-expect-error, which errors if the suppressed line no longer has an error:
// @ts-expect-error: Legacy API, will be fixed in v2
const result = oldApi.unsafeMethod();
If the error is fixed, TypeScript will warn that @ts-expect-error is unnecessary.
In rare cases where @ts-ignore is truly needed, disable ESLint for that line first, otherwise ESLint will automatically change it to @ts-expect-error.

Prefer unknown to any

Use unknown when a function doesn’t know the incoming type, not when it doesn’t care about the type.
unknown provides better type safety by requiring type guards:
const validateKey = (key: unknown): key is DictionaryKey => {
    if (['string', 'number'].includes(typeof key)) {
        return true;
    }

    return false;
};

const processKey = (key: unknown) => {
    if (validateKey(key)) {
        // TypeScript knows key is DictionaryKey here
        console.log(key.toUpperCase());
    }
};
Why unknown is better:
  • Forces explicit type checking
  • Catches errors at compile time
  • Provides better IntelliSense support
  • Makes type flow explicit

Type Definitions

Prefer Direct Type Assignment

Import types directly rather than accessing them indirectly.
import { NetworkSymbol } from '@suite-common/wallet-config';

const doSomething = (networkSymbol: NetworkSymbol) => {};
Benefits:
  • Easier to refactor if types change
  • Better editor navigation to type definitions
  • More readable and explicit
  • Prevents coupling to parent types

Prefer Types to Interfaces

Use type instead of interface for consistency.
Good
type User = {
    name: string;
    email: string;
};

type UserWithId = User & {
    id: string;
};
Avoid
interface User {
    name: string;
    email: string;
}

interface UserWithId extends User {
    id: string;
}
Rationale:
  • Consistency across the codebase
  • Types offer all necessary functionality
  • Simpler mental model
  • More details

Enums and Constants

Use Const Assertions Instead of Enums

TypeScript enums are not native to JavaScript and can behave unpredictably.
Use const assertions with objects instead: Benefits:
  • Native JavaScript objects
  • Predictable behavior
  • Can use both object values and string literals
  • Better tree-shaking
Learn more in this video explanation.

Defensive Programming

Force Explicit Return Types

Always specify return types to ensure all code paths are covered.
// TypeScript error: Function lacks ending return statement
export const isEnabled = (status: 'a' | 'b' | 'c'): boolean => {
    if (status === 'a') {
        return true;
    }

    if (status === 'b') {
        return false;
    }

    // Missing case 'c' - TypeScript catches this!
};

Use Exhaustive Switch Statements

Use the exhaustive helper to ensure all cases are handled.
import { exhaustive } from '@suite-common/utils';

// TypeScript error if case 'c' is not handled
export const isEnabled = (status: 'a' | 'b' | 'c') => {
    switch (status) {
        case 'a':
            return true;
        case 'b':
            return false;
        default:
            return exhaustive(status); // Ensures all cases are covered
    }
};
If a new case is added to the union type, TypeScript will error at the exhaustive() call.

Type-Mapping Technique

An alternative to exhaustive switches:
type Schema = {
    a: number;
    b: number;
};

// TypeScript error: Property 'b' is missing
const result: { [K in keyof Schema]: () => void } = {
    a: () => console.log('This is A'),
    // Missing 'b' - TypeScript catches this!
};

Error Handling

Do Not Use Exceptions for Expected Errors

Throwing exceptions is not type-safe. Use the Result type instead.
type Result<T, E> = 
    | { success: true; data: T }
    | { success: false; error: E };

type ErrorType = 'NetworkError' | 'ValidationError' | 'AuthError';

const action = async (): Promise<Result<string, ErrorType>> => {
    // Implementation
};

const result = await action();

if (!result.success) {
    const { error } = result;

    switch (error) {
        case 'NetworkError':
            // Handle network error
            break;
        case 'ValidationError':
            // Handle validation error
            break;
        case 'AuthError':
            // Handle auth error
            break;
        default:
            return exhaustive(error);
    }
}

// TypeScript knows result.data is available here
console.log(result.data);
Benefits of Result type:
  • Type-safe error handling
  • All possible errors are explicit
  • Compiler ensures all error cases are handled
  • No runtime surprises
Reserve exceptions for truly unpredictable failures (out of memory, system errors, etc.).

Type Guards

Create type-safe guards for runtime type checking:
type NetworkSymbol = 'btc' | 'eth' | 'ltc';

const isNetworkSymbol = (value: unknown): value is NetworkSymbol => {
    return ['btc', 'eth', 'ltc'].includes(value as string);
};

// Usage
const processNetwork = (input: unknown) => {
    if (isNetworkSymbol(input)) {
        // TypeScript knows input is NetworkSymbol here
        console.log(`Valid network: ${input}`);
    } else {
        console.log('Invalid network');
    }
};

Generic Types

Use descriptive generic names when the type is domain-specific:
type Account<TSymbol extends NetworkSymbol> = {
    symbol: TSymbol;
    balance: string;
};

type Transaction<TNetwork extends Network> = {
    network: TNetwork;
    amount: string;
};

Utility Types

Leverage TypeScript’s utility types:
// Pick specific properties
type UserProfile = Pick<User, 'name' | 'email'>;

// Omit specific properties
type UserWithoutPassword = Omit<User, 'password'>;

// Make all properties optional
type PartialUser = Partial<User>;

// Make all properties required
type RequiredUser = Required<User>;

// Make all properties readonly
type ReadonlyUser = Readonly<User>;

Type Assertions

Avoid type assertions when possible. Use type guards instead.
const processData = (data: unknown) => {
    if (isValidData(data)) {
        // TypeScript infers the correct type
        return data.value;
    }
};

Type Testing

Type tests should have .type-test.ts suffix to prevent Jest from executing them.
Example: packages/utils/tests/typedObjectFromEntries.type-test.ts
import { typedObjectFromEntries } from '../src/typedObjectFromEntries';

// Test that type inference works correctly
const obj = typedObjectFromEntries([
    ['a', 1],
    ['b', 2],
] as const);

// TypeScript should infer: { a: 1; b: 2; }
type Test = typeof obj;

Common Patterns

Discriminated Unions

Use discriminated unions for type-safe state management:
type LoadingState = 
    | { status: 'idle' }
    | { status: 'loading' }
    | { status: 'success'; data: string }
    | { status: 'error'; error: string };

const handleState = (state: LoadingState) => {
    switch (state.status) {
        case 'idle':
            return 'Not started';
        case 'loading':
            return 'Loading...';
        case 'success':
            return state.data; // TypeScript knows data exists
        case 'error':
            return state.error; // TypeScript knows error exists
        default:
            return exhaustive(state);
    }
};

Branded Types

Create nominal types for better type safety:
type AccountKey = string & { readonly __brand: 'AccountKey' };
type DeviceId = string & { readonly __brand: 'DeviceId' };

const createAccountKey = (key: string): AccountKey => key as AccountKey;
const createDeviceId = (id: string): DeviceId => id as DeviceId;

// Prevents mixing up different string types
const getAccount = (key: AccountKey) => { /* ... */ };

const accountKey = createAccountKey('acc-123');
const deviceId = createDeviceId('dev-456');

getAccount(accountKey); // ✓
getAccount(deviceId);   // ✗ Type error

Resources

TypeScript Handbook

Official TypeScript documentation

Code Style Guide

General coding conventions

Testing Guide

Testing TypeScript code

Defensive Programming

Type-safe error handling

Build docs developers (and LLMs) love