TypeScript stopped being an experimental technology for enthusiasts a long time ago. In 2026, it is the de facto standard for serious JavaScript development. The largest open-source projects—React, Vue, Angular, Next.js, and Prisma—are written in TypeScript or provide first-class support for types. Companies of all sizes are migrating their codebases to TypeScript, and the ability to use the type system effectively has become an expected skill for frontend and full-stack developers.
However, there is a gap between using TypeScript and using it effectively. Many developers stick to basic types and make liberal use of any, effectively turning TypeScript back into JavaScript with extra syntax. In this article, we’ll explore practices that help you get the most out of the type system.
Why strict typing matters
When a team starts using TypeScript, it is often tempting to configure the compiler as permissively as possible to minimize errors and speed up the migration of existing code. The strict option is disabled, any is used at the slightest difficulty, and types are often missing or inaccurate. The code compiles, the project works—so everything is fine, right?
In the short term, this approach really can help you move faster. But types don’t exist for the compiler—they exist for people. Types document the intent of the code, allow IDEs to provide accurate hints and autocompletion, and let refactoring tools change code safely. When types are inaccurate or missing, all these benefits are lost.
Moreover, type errors often point to logical errors in the code. When the compiler reports a type incompatibility, it’s not just a formality—it’s a signal that the code may behave unexpectedly in some scenarios. Suppressing these warnings with any or as unknown as Whatever puts off the problem until later, when it appears as a bug in production.
The recommendation for all new projects is clear: enable strict mode from day one. For existing projects, migrating to strict can be gradual, but it should remain a priority. The temporary inconvenience pays for itself many times over in long-term code reliability.
Type inference and when it isn’t enough
TypeScript has a powerful type inference system. In many cases, the compiler can determine the type of a variable, parameter, or return value without an explicit annotation. This makes code less verbose and easier to read.
However, type inference has its limits. When a function returns a value, TypeScript infers the type based on all possible return paths. If the logic is complex or includes conditions, the inferred type may be broader than the developer intended. An explicit return type not only documents intent but also lets the compiler verify that the implementation matches the specification.
It is especially important to type function parameters explicitly. Without types, the compiler has to use any or infer types from usage, which often leads to inaccurate results. Explicit parameter types make a function self-documenting—the reader can immediately understand what data the function expects and what it does with it.
Public APIs—exported functions, classes, and types—deserve particular attention when it comes to typing. These are the points of interaction between modules, and precise types are critical for preventing integration errors. Internal implementation details can rely on type inference, but interfaces should be explicit.
Use unknown instead of any
The any type effectively disables type checking. A variable of type any can contain anything, and TypeScript won’t complain about any operations performed on it. This is convenient in the moment, but dangerous: errors that the compiler should have caught slip into runtime.
The unknown type is a type-safe alternative to any. Like any, unknown can hold a value of any type. But unlike any, TypeScript won’t allow any operations on an unknown value until its type has been narrowed through runtime checks.
This distinction is critical when working with external data: API responses, user input, and values from localStorage. At compile time, these data really do have an unknown type, and unknown accurately reflects that reality. Using any in these cases creates the illusion of type safety where none exists.
When working with unknown, you need to implement type checks before using the value. This may seem like extra work, but these checks are exactly the code that should exist for reliable handling of external data. TypeScript simply makes them explicit and mandatory.
Discriminated unions
Type unions using the vertical bar operator are a powerful tool for modeling data that can take different forms. But simple unions have a limitation: TypeScript knows that a value is one of several types, but not which one, and requires checks for each possible type.
Discriminated unions solve this problem elegantly. Each member of the union contains a shared field with a unique literal value that lets us determine exactly which member we’re dealing with. When this field is checked, TypeScript automatically narrows the type to the specific member.
This pattern is especially useful for modeling states: loading, success, and error. Instead of several boolean flags that can be in invalid combinations, use a single object with a status or type discriminator. The code becomes more readable, and TypeScript guarantees that all possible states are handled.
Exhaustive checking is another advantage of discriminated unions. If a switch statement handles all members, TypeScript knows this. If a new member is added later, the compiler will point out every place where it hasn’t been handled. This prevents a class of errors related to incomplete handling of states.
Conditional and mapped types
TypeScript’s type system is Turing-complete, allowing complex type transformations at compile time. Conditional types check whether a type satisfies a particular condition and return different types depending on the result. Mapped types transform the properties of an existing type to create a new type based on it.
These capabilities are especially useful when creating libraries and utilities where types need to be as flexible and precise as possible. The utility types in the TypeScript standard library—Partial, Required, Pick, Omit, ReturnType, and others—are implemented using conditional and mapped types.
For everyday development, an in-depth knowledge of these mechanisms is not always necessary. But understanding how they work helps you use existing types more effectively and create your own utilities when the standard ones aren’t enough.
It’s important to strike a balance between expressive types and readability. A complex type that no one on the team can understand does more harm than good. If defining a type takes a page of code, it may be worth simplifying the data model or breaking the type into simpler components.
Typing asynchronous code
Asynchronous code is pervasive in modern JavaScript, and TypeScript provides tools for typing it. Promises are parameterized by their result type, async functions automatically wrap the returned value in a Promise, and await extracts the type from a promise.
Typing API responses requires special attention when working with external APIs. The server may return data in a format that differs from what was expected: a different object structure, missing fields, or null instead of a value. TypeScript cannot check whether runtime data matches the declared types—that is the developer’s responsibility.
Schema validation libraries such as Zod, Yup, or io-ts let you define a data schema that serves both for runtime validation and TypeScript type inference. This ensures consistency between declared types and actual checks, eliminating a class of errors caused by mismatches.
Error handling in asynchronous code also requires attention to types. TypeScript does not track which exceptions a function can throw—unlike languages such as Java with checked exceptions. When using try-catch, the error variable has the type unknown, which correctly reflects reality: we don’t know exactly what was thrown.
Typing React components
React applications are a common use case for TypeScript, and there are some specific considerations and best practices here.
Component props are naturally typed using interfaces or types. It’s important to decide which props are required and which are optional, and use the appropriate syntax. Default values for optional props can be specified through destructuring in the function parameters.
Children require special attention. The React.ReactNode type covers everything React can render: elements, strings, numbers, arrays, fragments, and null. If a component expects a specific type of children—for example, only a function or a single element—the type should reflect that.
The useState and useReducer hooks infer types from the initial value, but sometimes an explicit annotation is needed. When the initial value is null but an object is expected later, the type must include both possibilities. For useReducer, the action and state types define the reducer’s contract.
Refs are typed using the useRef generic parameter. When working with DOM elements, types such as HTMLInputElement and HTMLDivElement are used. For refs that hold arbitrary values, specify the value’s type.
Organizing types in a project
As a project grows, the number of types increases, so it’s important to organize them sensibly. Types used in only one file should be defined in that file. Types shared between several files in the same module can be moved to a separate types file within the module. Global types used throughout the application belong in a shared types directory.
Types are explicitly exported from a file using export type or export interface. This is important for libraries where types are part of the public API. It’s recommended to use import type when importing types, which allows the bundler to remove the import entirely from the output code.
Type declarations for libraries without built-in typing can be installed from DefinitelyTyped using packages with the @types prefix. For internal libraries or specific cases, you can create your own declaration file.
Documenting types with JSDoc comments improves the experience of developers who use them. Comments appear in the IDE on hover and during autocompletion, providing context without the need to look at the source code.
Conclusion
In 2026, TypeScript is not just JavaScript with types. It is a tool for designing, documenting, and verifying code that, when used correctly, significantly improves software quality.
The key to using TypeScript effectively is understanding that types serve people, not the compiler. Precise types make code understandable, maintainable, and resilient to change. Inaccurate types create a false sense of security and accumulate technical debt.
Invest time in learning the type system. Use strict mode. Avoid any where unknown is possible. Model data with discriminated unions. Validate external data. These practices require discipline, but they reward you with code that works predictably and evolves easily.