Skip to main content

TypeScript

What happens when you tell the compiler what you believe about your code?

TypeScript is JavaScript with a belief system. You state what you believe about your code. The compiler checks those beliefs for contradictions. Those contradictions are type errors. Stating beliefs more precisely means more precise error messages — which makes debugging and maintenance easier.

Why It Compounds

Code must be understandable by programmers who join the team in the future and don't know the history. TypeScript makes implicit contracts explicit.

Without TypesWith Types
Runtime crashes reveal shape mismatchesCompiler catches them before running
Domain rules live in comments or tribal knowledgeDomain rules live in the type system
Refactoring is grep-and-prayRefactoring is compiler-guided
API boundaries are trust-basedAPI boundaries are contract-based

In a monorepo, TypeScript is the language AND the boundary enforcement mechanism. Type-first development makes the compiler the methodology — domain types first, then layer-by-layer constraint satisfaction.

Monorepo Config

TypeScript performance in monorepos lives or dies by project references. Get this wrong and you get cascading errors that tempt you into disabling safety — which makes everything worse.

SettingRuleWhy
composite: trueOn all library tsconfigsEnables incremental builds, declaration emit
declaration: truePaired with compositeDownstream projects resolve from .d.ts, not source
rootDirMust align with include and outDirMisalignment causes TS6059 in CI
skipLibCheckNever in root configMasks declaration mismatches between libraries
strict: trueAlwaysLoosening strict trades safety for silence

Never import across lib boundaries via src/ paths. Always use the package alias. Cross-boundary src/ imports bypass project references and break the Nx project graph.

Error Decoder

ErrorMeaningFix
TS6305Importing a project whose .d.ts output is missing or staleEnsure composite: true, build deps first, import via package alias
TS6059File is not under rootDirAlign rootDir, include, outDir in tsconfig.lib.json
TS2305Module has no exported memberBarrel surface changed but consumers still import old names

See Engineering Quality Benchmarks for measurable type safety thresholds and Engineering Anti-Patterns for common TypeScript config violations.

Best Practices

  • Union types over enums — enums generate runtime code, unions are zero-cost
  • No default in switch unless needed — it negates exhaustiveness checking
  • extends over intersections — intersections produce confusing error messages
  • Object literals only specify known properties

Pitfalls

PatternProblem
anyDisables type checking entirely — a single any propagates
as type assertionsOverrides the compiler's judgment — dangerous unless narrowing from unknown
Nested export *Defeats tree-shaking, creates barrel blowouts in monorepos
@ts-ignoreSilences errors without fixing them — use @ts-expect-error if truly needed

Type Patterns

Keyof with Typeof

Modifications to the source object are handled by the compiler automatically.

const icons = {
rightArrow: "fake right arrow image",
leftArrow: "fake left arrow image",
billing: "fake billing image",
};

function Icon(props: { name: keyof typeof icons }) {
return icons[props.name];
}

typeof icons gives the object shape. keyof typeof icons gives 'rightArrow' | 'leftArrow' | 'billing'. Add a new icon, the union updates. Remove one, consumers break at compile time.

Assertion Functions

Assertion functions throw unless a condition is true, narrowing the type for all subsequent code.

function assertNumber(n: unknown): asserts n is number {
if (typeof n !== "number") {
throw new Error("Value wasn't a number: " + n);
}
}

Type guards narrow inside a code block. Assertion functions narrow for everything after the call.

warning

Write automated tests for assertion functions — they bypass the compiler's judgment.

Covariance vs Contravariance

Assignability is covariant for values (narrow assigns to wide) but contravariant for function parameters (wide assigns to narrow).

type TakesLiteralString = (s: "lastLoginDate") => string;

function takesString(s: string): string {
return s;
}

// Safe: takesString accepts any string, including "lastLoginDate"
const testFunction: TakesLiteralString = takesString;
testFunction("lastLoginDate");

The variable restricts callers to 'lastLoginDate'. The underlying function handles any string. Narrower call site, wider implementation — safe by construction.

Overloading

Useful when return type depends on input shape.

interface User { id: number; name: string }
interface UserWithComments extends User { comments: { subject: string }[] }

function findUser(id: number): User;
function findUser(id: number, options: { withComments: true }): UserWithComments;
function findUser(id: number, options?: { withComments?: boolean }): User | UserWithComments {
const user = { id: 1, name: "Amir" };
if (options?.withComments) {
return { ...user, comments: [{ subject: "Ms. Fluff's 4th birthday" }] };
}
return user;
}

Dig Deeper

Context

Resources

tip

Use Execute Program to get the fundamentals in your fingers.