· 8 min read
The Social Code: How TypeScript Hacks Foster Collaboration and Code Reviews
Explore a set of practical TypeScript 'hacks'-patterns, config tweaks, and tool integrations-that improve team communication, make intentions explicit, and massively reduce back-and-forth during code reviews.
Introduction
When many hands touch the same codebase, the hardest problems are rarely technical - they’re social. TypeScript is uniquely positioned to reduce social friction because types are shared, machine-checked contracts that live next to implementation. By applying a set of practical TypeScript patterns and tooling “hacks,” teams can make intent explicit, reduce ambiguity in pull requests, and speed up reviews.
This article collects concrete techniques you can adopt today. Each entry explains the idea, shows a concise example, and explains how it helps team communication and code reviews.
Why types are social glue
- Types make intent explicit: reviewers no longer have to infer expected shapes from tests or runtime code.
- Types are executable documentation: IDE hovers and autocompletion surface intent without opening extra files.
- Types prevent classes of bugs early: fewer silly runtime surprises means less subjective debate in PR comments.
References: the official TypeScript Handbook is a great general reference: https://www.typescriptlang.org/docs/.
Hack 1 - Shared strict tsconfig: make the contract strict and visible
Put a canonical tsconfig.json at the repo root and opt into strictness. Small, consistent settings reduce subjective style arguments during reviews.
Example tsconfig.json (relevant bits):
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"strict": true,
"noImplicitAny": true,
"exactOptionalPropertyTypes": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
Why this helps reviews
- A reviewer’s checklist becomes objective: “Does it type-check under the repo tsconfig?” instead of debating design choices.
- Prevents stealthy
any
regressions that would otherwise surface as runtime surprises.
Hack 2 - Nominal (branded) types to prevent domain mistakes
Primitive types (string, number) are easy to mix up in a large codebase (userId vs orderId). A branded type gives a tiny compile-time guarantee.
Example:
type Brand<K, T> = K & { __brand: T };
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;
function mkUserId(s: string): UserId {
return s as UserId;
}
function sendReminder(userId: UserId) {}
const uid = mkUserId('u123');
// sendReminder("u123"); // Error
sendReminder(uid); // OK
Why this helps reviews
- Prevents subtle, semantic bugs. Reviewers can see the domain intent in the type system rather than relying on naming alone.
- A reviewer can confidently accept code that compiles, knowing the compiler enforces domain boundaries.
Hack 3 - Discriminated unions + exhaustive checks
Make branching logic explicit with discriminated unions and enforce exhaustiveness using a helper like assertNever. This pattern makes adding new cases an obvious review point.
Example:
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'rect'; width: number; height: number };
function area(s: Shape) {
switch (s.kind) {
case 'circle':
return Math.PI * s.radius ** 2;
case 'rect':
return s.width * s.height;
default: {
const _exhaustive: never = s;
return _exhaustive;
}
}
}
Why this helps reviews
- When a new variant is added, TypeScript forces you to update all switch statements. Reviewers immediately see if a file hasn’t been updated because it will fail to compile.
Hack 4 - Type-level tests (tsd) for communicated guarantees
Type assertions in unit tests (not just runtime behavior tests) let the code explicitly encode the type guarantees you rely on. Use a library like tsd to assert types.
Example (pseudo):
// tests/types.test-d.ts
import { expectType } from 'tsd';
import { fetchUser } from '../src/api';
expectType<Promise<User>>(fetchUser('u1'));
Project: https://github.com/SamVerschueren/tsd
Why this helps reviews
- Types-as-tests are part of CI. If a refactor changes an exported shape, type tests fail and reviewers see an objective failing test rather than a long-threaded discussion.
Hack 5 - Share runtime + type schema using Zod or io-ts
When the data shape matters at runtime and compile-time, keep schema and types in sync by defining a single schema and deriving the TypeScript type from it. Zod is a popular choice.
Example with Zod:
import { z } from 'zod';
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
});
type User = z.infer<typeof UserSchema>;
// runtime
const maybeUser = UserSchema.safeParse(JSON.parse(someJson));
Zod: https://github.com/colinhacks/zod
Why this helps reviews
- A single source of truth prevents mismatches between runtime validators and TypeScript types - reviewers can reason about both easily.
Hack 6 - Make types part of the PR narrative
Encourage PR authors to include type-level notes in the PR description: what type changes were made, API surfaces affected, and any newly exported types. This makes review scope explicit.
Suggested PR template excerpt:
- Types changed: (none) / (describe)
- New domain types: (list)
- Backwards-incompatible type changes: yes/no
Why this helps reviews
- Reduces the cognitive load for reviewers. They don’t need to hunt for type changes - the author calls them out explicitly.
Hack 7 - Enforce type-checks in CI and pre-merge
Add an explicit type-check CI job (tsc —noEmit) and fail the pipeline on type errors. This elevates type correctness to the same level as tests.
GitHub Actions snippet:
jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 18
- run: npm ci
- run: npm run typecheck # e.g. tsc -p tsconfig.json --noEmit
Why this helps reviews
- Reviewers can rely on CI to catch type regressions. It shortens the review loop - if CI is green, certain classes of comments are unnecessary.
Hack 8 - Type-aware ESLint rules and linting in PRs
Use @typescript-eslint to enable type-aware lint rules (some rules require type information from TS). Examples: no-floating-promises, strict-boolean-expressions (via additional plugins), consistent-type-definitions.
Why this helps reviews
- ESLint provides deterministic, auto-fixable rules that avoid stylistic argumentation in reviews. With type-aware rules enabled, lints catch subtle type-related smells.
See: https://github.com/typescript-eslint/typescript-eslint
Hack 9 - Pre-commit hooks: run quick checks locally
Husky + lint-staged to run fast checks (formatting, ESLint) pre-commit, and optionally run a focused type-check on changed files. This reduces trivial fixup commits that clutter reviews.
package.json scripts example:
{
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.ts": ["eslint --fix", "prettier --write"]
}
}
Husky: https://github.com/typicode/husky lint-staged: https://github.com/okonet/lint-staged
Why this helps reviews
- Reviewers see cleaner diffs (formatting already applied) and fewer noise comments about style.
Hack 10 - Use utility types to make refactors less scary
Common utility types (ReturnType, Parameters, Omit, Pick) create tiny, explicit contracts that remove guesswork during changes.
Example: typed event emitter
type Events = {
userCreated: { id: string; name: string };
userDeleted: { id: string };
};
class Emitter<E extends Record<string, any>> {
on<K extends keyof E>(ev: K, cb: (payload: E[K]) => void) {}
}
const bus = new Emitter<Events>();
// bus.on('userCreated', payload => { payload.id; /* strongly typed */ });
Why this helps reviews
- A reviewer can read a compact type and immediately know what payloads are expected; no need to chase through the codebase.
Hack 11 - Use readonly and as const for intent
Prefer readonly arrays/objects and as const where appropriate to communicate immutability.
const ROLES = ['admin', 'editor', 'viewer'] as const;
type Role = (typeof ROLES)[number]; // "admin" | "editor" | "viewer"
const config: Readonly<{ maxUsers: number }> = { maxUsers: 100 };
Why this helps reviews
- Makes intent explicit: reviewers won’t wonder whether a function mutates a shared structure.
Hack 12 - Runtime assertions that narrow types (asserts)
TypeScript’s assertion signatures let you write runtime checks that also narrow types for callers. They document and enforce runtime assumptions.
function assertIsUser(x: any): asserts x is User {
if (!x || typeof x.id !== 'string') throw new Error('Not a user');
}
function greet(x: any) {
assertIsUser(x);
// From here, x is typed as User
console.log(`Hello ${x.name}`);
}
Why this helps reviews
- Makes runtime guarantees explicit, reducing discussion about whether a function can receive bad data.
Hack 13 - Project references and path aliases in monorepos
Use TypeScript project references in large repos to make per-package builds fast and to express package boundaries clearly. Combine with path aliases for readable imports.
Why this helps reviews
- Reviewers can quickly reason about whether a change crosses package boundaries and whether that change affects public types.
A concise reviewer checklist
When reviewing a TypeScript PR, run through this quick checklist:
- Does it compile under the shared tsconfig? (CI passes)
- Are there any new or changed exported types? Are they documented in the PR?
- Are there any any/unknown escapes? If unavoidable, is the rationale documented?
- Do discriminated unions and switches include exhaustive handling? (no silent fallthrough)
- Are runtime validators and TypeScript types in sync for external inputs?
- Are lints and formatters green? (no noisy fix commits required)
- Are type-level tests (if present) updated and passing?
Adoption path for teams
- Start with shared tsconfig + a CI typecheck job.
- Add @typescript-eslint and a small set of strict rules that the team agrees on.
- Introduce branded types and discriminated unions gradually for high-risk areas.
- Add type-tests (tsd) for public packages and critical invariants.
- Encourage PR authors to call out type changes in the PR description.
Further resources
- TypeScript Handbook: https://www.typescriptlang.org/docs/
- typescript-eslint: https://github.com/typescript-eslint/typescript-eslint
- tsd (type tests): https://github.com/SamVerschueren/tsd
- Zod: https://github.com/colinhacks/zod
- Husky: https://github.com/typicode/husky
- lint-staged: https://github.com/okonet/lint-staged
Conclusion
TypeScript does more than prevent runtime errors: it turns code into a shared language for a team. Small, pragmatic “hacks” - from strict configs and branded types to type-driven tests and CI enforcement - make PRs more objective, reduce opinion-based comments, and lead to faster, kinder reviews. Start small, prioritize the low-friction wins, and your next code review will be less about guessing intent and more about shipping value.