Writing / 2022

TypeScript Best Practices From a Go Developer

TypeScript is the best thing to happen to JavaScript, and that bar was low. Strict mode, boundary validation, and simple generics for large codebases.

I write Go most of the time. When I work on TypeScript projects, which happens regularly, I appreciate what it brings to the JavaScript ecosystem. I also think the community overcomplicates it. Generics four levels deep, utility type gymnastics, type-level programming that requires a PhD to review. The language is at its best when the types are simple and the boundaries are strict.

Here is what I think actually matters for large TypeScript codebases, from someone who would rather be writing Go.

Turn on strict mode. Keep it on.

{
  "compilerOptions": {
    "strict": true
  }
}

This is non-negotiable. strict: true catches null errors, implicit any types, and a whole class of runtime surprises. If your project doesn’t have this enabled, every type annotation is half a lie because the compiler isn’t actually enforcing the contract.

Retrofitting strict mode into an existing codebase is painful. The gradual route is one flag at a time, as I described in migrating to TypeScript . Do it early or pay for it later. There’s no third option.

Validate at the boundaries, trust the interior

The biggest insight from Go that translates directly to TypeScript: treat external data as hostile. API responses, form inputs, queue messages, file contents: all of it is unknown until you validate it.

function parseUser(data: unknown): User {
  if (typeof data !== "object" || data === null) {
    throw new Error("expected object");
  }
  if (!("id" in data) || !("email" in data)) {
    throw new Error("missing required fields");
  }
  return data as User;
}

Use a validation library like Zod if you want something less manual. The point is: validate once at the edge, then keep the interior of your codebase strongly typed. No any leaking through from API calls. No as unknown as Whatever casts to paper over the fact that you don’t know what shape the data is.

Discriminated unions are the best feature

This is the one thing TypeScript does better than Go. (I said what I said.)

type Result<T> =
  | { ok: true; value: T }
  | { ok: false; error: string };

Switch on the discriminant, add a never check in the default branch, and the compiler enforces exhaustiveness. No forgotten error branches. No if err != nil, although I’ve come to appreciate Go’s explicitness there too.

Use discriminated unions for state machines, API responses, and anything with multiple possible shapes. They’re readable, the tooling support is excellent, and they prevent an entire category of bugs.

Stop overusing generics

This is where TypeScript culture drives me up a wall. I regularly see utility types like DeepPartial<Omit<Pick<T, K>, "id"> & Required<Whatever>> in production code. Nobody can read that. Nobody can review it confidently. It’s type-level cleverness for its own sake.

One of the Go proverbs is “Clear is better than clever.” The same should apply to TypeScript generics. Constrain your type parameters. Keep generic functions to one or two type parameters. If a type definition needs a comment to explain what it does, simplify it.

Keep modules clean

Cyclic dependencies are the slow death of large TypeScript projects. Use type-only imports to make intent clear:

import { createUser } from "./user.js";
import type { User } from "./user.js";

Export types and values from the same module when they belong together. Don’t create a types.ts file that every module imports from. That’s a dependency magnet.

Where TypeScript earns its keep

TypeScript is a significant improvement over JavaScript for anything beyond a small script. The compiler catches real bugs. Discriminated unions and strict null checks prevent real production incidents. The tooling is genuinely good.

But unlike Go or Rust, TypeScript’s type system is deliberately unsound. It’s bolted onto a dynamic language, with escape hatches everywhere. any, type assertions, @ts-ignore: the temptation to bypass the system is always one keystroke away.

The discipline has to come from the team. Strict mode on, any banned in lint rules, boundaries validated, generics kept simple. Do that and TypeScript earns its complexity budget. Skip it and you get all the overhead of a type system with half the guarantees.

References