TypeScript Is the Only Frontend Language That Matters
We evaluated every compile-to-JS language. TypeScript won because the type system is a programming language unto itself.
We evaluated ReScript, Elm, PureScript, Dart, ClojureScript, and Kotlin/JS. We picked TypeScript. Not because it is the safest. Not because it is the most elegant. Because it is the only language where the type system is powerful enough to encode our domain constraints and the ecosystem is fast enough to not slow us down.
TypeScript's type system is Turing-complete. That is not a theoretical curiosity. We use it every day to catch bugs at compile time that other languages catch at runtime, or not at all.
Structural Typing Is Correct for UI
Elm and PureScript use nominal typing. A UserId and a SegmentId are different types even if they are both strings. This sounds disciplined. In practice, it creates an explosion of newtype wrappers, unwrap functions, and boilerplate that makes UI code verbose without making it meaningfully safer.
UI code is fundamentally about data flowing through components. A component does not care where a piece of data came from. It cares about the shape of that data. Structural typing matches this reality. If a component needs an object with { name: string; avatar: string }, any object with those fields works. You do not need to convert between UserProfile and DisplayProfile when the shapes are compatible.
Nominal typing makes sense in systems code where you want to prevent accidentally passing a user ID where a session ID is expected. We use Zig for that. In UI code, structural typing reduces friction without reducing safety, because the type checker still validates every field access and every function call.
We enforce strictness where it matters through branded types and template literal types, not through a nominal type system that taxes every line of code.
Advanced Patterns We Actually Use
Template Literal Types for Design Tokens
Our design system defines spacing, colors, and typography as constrained string types:
type Space = `${number}px` | `${number}rem`;
type Color = `#${string}` | `rgb(${number}, ${number}, ${number})` | `hsl(${number}, ${number}%, ${number}%)`;
type FontWeight = 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
interface StyleProps {
padding?: Space;
color?: Color;
fontWeight?: FontWeight;
}
You cannot pass "blue" as a color. You cannot pass "big" as a spacing value. The compiler catches it. This eliminates an entire category of visual bugs that CSS-in-JS libraries normally let through.
Conditional Types for API Response Validation
Our ad-serving dashboard consumes APIs with polymorphic responses. The response shape depends on the request parameters. We encode this dependency in the type system:
type ApiResponse<T extends ApiRequest> =
T extends { endpoint: "campaigns"; include: "metrics" } ? CampaignWithMetrics[] :
T extends { endpoint: "campaigns" } ? Campaign[] :
T extends { endpoint: "creatives"; format: infer F } ? Creative<F>[] :
T extends { endpoint: "segments" } ? Segment[] :
never;
function fetchApi<T extends ApiRequest>(request: T): Promise<ApiResponse<T>> {
// implementation
}
The caller gets the exact return type based on the request they pass in. No type assertions. No as unknown as. The type system computes the correct return type at compile time.
Mapped Types for Form State
Every form in our dashboard is derived from an API schema type. We do not duplicate types:
type FormState<T> = {
[K in keyof T]: {
value: T[K];
error: string | null;
touched: boolean;
dirty: boolean;
};
};
type CampaignForm = FormState<Pick<Campaign, "name" | "budget" | "targeting">>;
If the Campaign type changes, every form that references it updates automatically. The compiler tells us exactly which form components need to handle the new field. This is not a framework feature. This is the type system doing structural computation.
Zero-Any Enforcement
We run TypeScript with "strict": true and a custom ESLint rule that treats any occurrence of the any type as a build failure. Not a warning. A failure. The CI pipeline will not merge code that contains any.
This sounds extreme. It is. Here is why: any is a type-system escape hatch that propagates. One any in a function signature infects every caller. A single any in a utility function used by 30 components means 30 components have an untyped boundary. We have seen codebases where any usage started at 2% of type annotations and grew to 15% within a year. At 15%, the type system is no longer providing meaningful guarantees.
Our policy for when the type system cannot express what you need: use unknown with explicit type guards, or use branded types with runtime validation at the boundary. Both are more work than any. Both actually work.
We have 247,000 lines of TypeScript in our frontend monorepo. Zero uses of any. The type-coverage tool reports 100% coverage. Build times average 4.2 seconds for a full type check.
The Ecosystem Is the Moat
The reason we did not pick Elm, PureScript, or ReScript is not that those languages are bad. Elm's architecture is beautiful. PureScript's type system is more powerful than TypeScript's. ReScript's compiler is faster.
None of that matters if the ecosystem cannot keep up.
Our frontend stack: React 19 with server components, TanStack Router for type-safe routing, TanStack Query for data fetching, Vite 6 for builds, oxc for linting (10x faster than ESLint on our codebase), and Bun for package management and test running. Every one of these tools is TypeScript-first. The type definitions are not an afterthought; they are the primary API.
TanStack Router gives us type-safe route parameters. If a route expects { campaignId: string }, the link component will not compile without that parameter. Try getting that in Elm. You will be writing your own router.
Vite's HMR updates our 247k-line codebase in under 200ms. The developer feedback loop is faster than our developers can context-switch. This matters because every second of build time is a second of broken flow state, and flow state is where the hard problems get solved.
oxc parses TypeScript at 2.8GB/s on our hardware. Our entire codebase lints in 340ms. We run the linter on every keystroke via editor integration. This is what happens when the tooling is written in Rust by people who care about performance.
Bun installs our 1,847 dependencies in 1.2 seconds from a warm cache. npm install took 28 seconds. That is a 23x improvement on an operation that every developer runs multiple times per day.
Why Not the Others
ReScript: Excellent compiler performance, but the ecosystem is small. Finding a production-quality charting library, a drag-and-drop library, and an accessibility-compliant component library in the ReScript ecosystem is an exercise in frustration. We would end up writing JavaScript FFI bindings for everything, which eliminates the type-safety benefit.
Elm: No FFI by design. This is principled and admirable. It also means we cannot use the 2.3 million npm packages that represent 15 years of collective frontend engineering effort. Elm's package ecosystem has roughly 1,800 packages. TypeScript's has 2.3 million. The ratio speaks for itself.
PureScript: The most powerful type system of any compile-to-JS language. Also the steepest learning curve. We hire 6-8 frontend engineers per year. Finding engineers who are productive in PureScript within their first month is not realistic. Finding engineers who are productive in TypeScript within their first week is trivial, because most of them already know it.
Dart: Flutter on the web is impressive for app-like experiences. Our dashboard is document-like with complex data tables, charts, and forms. The web platform's native layout engine (CSS Grid, Flexbox) handles this better than Flutter's widget tree. We are not fighting the platform.
TypeScript is not the most elegant language. It is not the safest. It is not the fastest to compile. It is the language where the type system is expressive enough, the ecosystem is large enough, the tooling is fast enough, and the hiring pool is deep enough. In engineering, "enough" across every dimension beats "excellent" in one dimension with crippling gaps in the others.
We write our infrastructure in Zig for control. We write our backend pipelines in Gleam for concurrency. We write our frontend in TypeScript for pragmatism. The right tool for the right job is not a cliche. It is an engineering discipline.