DEV Community

TypeScript Won β€” Now Stop Using It Like Fancy JavaScript

Modern Web Development in 2026

A practical series about building faster, cleaner, more maintainable web applications without chasing every shiny thing.

TypeScript won the argument.

Most serious JavaScript codebases either use it already or are planning around it. The more interesting question is what we do after adoption.

Because a lot of TypeScript code is still just JavaScript wearing a blazer.

function createUser(data: any): any {
  return api.post("/users", data);
}
Enter fullscreen mode Exit fullscreen mode

Technically TypeScript. Spiritually not.

Types should protect boundaries

The best places to spend type effort are boundaries:

  • API input,
  • API output,
  • database records,
  • form state,
  • configuration,
  • feature flags,
  • permissions,
  • domain events.

Internal implementation types are useful. Boundary types are where bugs go to die.

Replace vague shapes with domain language

This is common:

type User = {
  id: string;
  role: string;
  status: string;
};
Enter fullscreen mode Exit fullscreen mode

It is better than nothing, but it leaves too much room.

type UserRole = "admin" | "editor" | "viewer";
type UserStatus = "invited" | "active" | "suspended";

type User = {
  id: UserId;
  role: UserRole;
  status: UserStatus;
};
Enter fullscreen mode Exit fullscreen mode

Now invalid states have fewer places to hide.

Use branded types for dangerous strings

Not every string is the same kind of string.

type Brand<T, Name extends string> = T & { readonly __brand: Name };

type UserId = Brand<string, "UserId">;
type OrgId = Brand<string, "OrgId">;
Enter fullscreen mode Exit fullscreen mode

Now this mistake becomes harder:

function getUser(orgId: OrgId, userId: UserId) {}
Enter fullscreen mode Exit fullscreen mode

A plain string can come from anywhere. A branded string says, β€œThis value passed through a specific gate.”

Prefer discriminated unions over boolean soup

Boolean-heavy state is where UI bugs breed.

type RequestState = {
  loading: boolean;
  error?: string;
  data?: User;
};
Enter fullscreen mode Exit fullscreen mode

This allows nonsense:

{ loading: true, error: "Failed", data: user }
Enter fullscreen mode Exit fullscreen mode

Use a union:

type RequestState =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: User }
  | { status: "error"; message: string };
Enter fullscreen mode Exit fullscreen mode

Now the impossible states are actually impossible.

Validate runtime data

TypeScript does not validate JSON at runtime.

This is one of the most expensive misunderstandings in web development.

const user = (await response.json()) as User;
Enter fullscreen mode Exit fullscreen mode

That line does not make the response a User. It makes the compiler quiet.

Use runtime validation at external boundaries:

const UserSchema = z.object({
  id: z.string(),
  role: z.enum(["admin", "editor", "viewer"]),
  status: z.enum(["invited", "active", "suspended"])
});

const user = UserSchema.parse(await response.json());
Enter fullscreen mode Exit fullscreen mode

Now TypeScript and runtime reality are connected.

Use satisfies for configuration

Configuration should be checked without losing literal types.

const routes = {
  home: "/",
  dashboard: "/dashboard",
  settings: "/settings"
} satisfies Record<string, `/${string}`>;
Enter fullscreen mode Exit fullscreen mode

This catches invalid values while preserving useful inference.

Avoid type theater

Some types add noise but not safety.

type StringOrNumber = string | number;
type Callback = (...args: any[]) => any;
type ObjectMap = Record<string, any>;
Enter fullscreen mode Exit fullscreen mode

These can be valid in rare cases, but often they are placeholders for decisions nobody made.

Good TypeScript is not more TypeScript. Good TypeScript is better constraints.

The practical checklist

Use this before merging TypeScript-heavy changes:

  • [ ] Are external inputs validated at runtime?
  • [ ] Are domain concepts named explicitly?
  • [ ] Are impossible UI states impossible?
  • [ ] Are IDs and tokens distinguishable?
  • [ ] Are any and unsafe assertions isolated?
  • [ ] Can a new developer understand the model from the types?
  • [ ] Do the types reduce tests you would otherwise need?

Final thought

TypeScript is not valuable because it makes JavaScript look professional.

It is valuable because it lets you encode decisions before those decisions become bugs.

If your types do not protect boundaries, model the domain, or remove impossible states, you are leaving most of the value on the table.

Sources


Thanks for reading.

You can find me here:

Top comments (0)