How We Write TypeScript
August 28, 2025 ยท View on GitHub
This guide provides conventions for writing readable, idiomatic, and testable TypeScript code in this codebase.
It covers our primary tools, patterns, and standards to help you contribute high-quality code in every pull request.
Guiding Principles
We follow standard TypeScript best practices, with a focus on simplicity, readability, and type safety. When in doubt, refer to the official TypeScript Handbook and existing code in this repository.
Key resources include:
Prefer Plain Objects over Classes
Prefer the use of plain JavaScript objects with accompanying TypeScript interface or type declarations over JavaScript class syntax.
- Reduced Boilerplate: Classes often promote the use of constructors,
thisbinding, getters, setters, and other boilerplate that can unnecessarily bloat code. - Enhanced Readability: Plain objects, especially when their structure is clearly defined by TypeScript interfaces, are often easier to read and understand.
- Simplified Immutability: Plain objects encourage an immutable approach to data.
- Better Serialization: Plain JavaScript objects are naturally easy to serialize to JSON and deserialize back.
Embrace ES Module Syntax for Encapsulation
Leverage ES module syntax (import/export) for encapsulating private and
public APIs.
- Clear Public API Definition: Anything that is exported is part of the public API of that module, while anything not exported is inherently private to that module.
- Enhanced Testability: Unexported functions or variables are not accessible from outside the module. This encourages you to test the public API of your modules, rather than their internal implementation details.
- Reduced Coupling: Explicitly defined module boundaries through
import/exporthelp reduce coupling between different parts of your codebase. -Faster Module Loading: ESM asynchronous module loading allows for parallel loading of import modules, as well as tree-shaking, giving our code more efficient loading.
Code Style and Linting
We use ESLint for linting and Prettier for code formatting. Presubmit checks will fail if code is not properly formatted.
Before submitting any changes, it is crucial to validate them by running the full preflight check. This command will build the repository, run all tests, check for type errors, and lint the code.
To run the full suite of checks, execute the following command:
npm run preflight
Type Safety
Leverage TypeScript's type system to make your code more robust and self-documenting.
- Use specific types: Avoid
anywhenever possible. Prefer specific types likestring,number, or custom interfaces. - Use
unknownfor safety: When dealing with values from external sources (e.g., APIs, user input), useunknownand perform type checking before use. - Keep types close to the code they describe: Define interfaces and types near the functions or classes that use them to improve locality and readability.
Avoid any Types and Type Assertions; Prefer unknown
Using any effectively opts out of TypeScript's type checking for that
particular variable or expression. When you absolutely cannot determine the type
of a value at compile time, and you're tempted to reach for any, consider
using unknown instead. unknown is a type-safe counterpart to any.
Embrace JavaScript's Array Operators
Leverage JavaScript's rich set of array operators as much as possible. Methods
like .map(), .filter(), .reduce(), .slice(), .sort(), and others are
incredibly powerful for transforming and manipulating data collections in an
immutable and declarative way.
Writing Tests
We use Vitest for testing. Tests should be clear, concise, and focused on a single behavior.
Tests should also be focused on outcomes rather than the specific implementation. This gives testing greater confidence if an implementation is changed, but the outcome remains the same.
Naming and Location
- Test files must be located alongside the code they test.
- Test files must be named with a
.test.tssuffix (e.g.,my-feature.test.ts).
Test Structure
Use describe to group related tests and test (or it) for individual test
cases. This improves organization and reporting.
Example:
import { describe, expect, test } from 'vitest';
import { Calculator } from './calculator';
describe('Calculator', () => {
test('adds two numbers', () => {
const calc = new Calculator();
expect(calc.add(2, 3)).toBe(5);
});
test('subtracts two numbers', () => {
const calc = new Calculator();
expect(calc.subtract(5, 3)).toBe(2);
});
});
Mocking (vi from Vitest)
- ES Modules: Mock with
vi.mock('module-name', async (importOriginal) => { ... }). - Mocking Order: For critical dependencies (e.g.,
os,fs) that affect module-level constants, placevi.mockat the very top of the test file, before other imports. - Mock Functions: Create with
vi.fn(). - Spying: Use
vi.spyOn(object, 'methodName'). Restore spies withmockRestore()inafterEach.
Need Help?
This guide is a living document. If you have questions or suggestions, please open an issue or start a discussion.