10 TypeScript Features I Can't Imagine Working Without as a Frontend Developer
Learn about TypeScript features that help you write safer and more maintainable code—from generics to discriminated unions.
When I started working with TypeScript, it was a mystery to me, and I didn't really understand what it was for. What a shame, I was using the "any" type everywhere. The compiler was "happy," I was happy, all the tests passed, and it was time for CS. Then I matured a bit and started using it as God intended. I treated it mainly as an extra layer of security.
"Is this variable a string?"
"Does this prop exist?"
"Did I misspell the field name?"
And indeed, that alone can save a lot of time.
But after a few years of working with React and Next.js, I've noticed something more important:
TypeScript isn't just a tool for finding bugs.
Used well, TypeScript helps you design your application.
It helps you determine:
- what data can flow through the system,
- what states are possible,
- where a component should be flexible,
- and where it's better to limit the possibility of change.
In larger projects, types often become the first architectural documentation.
Below are the 10 TypeScript features I use most frequently in my daily work.
1. Generics: The Foundation of Scalable Components and Hooks
Generics were one of the moments when TypeScript really started to click for me.
In React, we often create things that work for different data types.
Example: a table component.
We don't want to write:
<UserTable users={users} />
<ProductTable products={products} />
and create two nearly identical components.
We can create a generic table:
type TableProps<T> = {
data: T[];
renderRow: (item: T) => React.ReactNode;
};
function Table<T>({ data, renderRow }: TableProps<T>) {
return (
<div>
{data.map(renderRow)}
</div>
);
}Now TypeScript knows what data types the component supports.
I use the same approach for:
- custom hooks,
- API wrappers,
- forms,
- UI components.
Generics allow you to write reusable code without losing security.
2. Utility Types: less duplication, more consistency
In front-end applications, we often have the same data model in multiple places.
Example:
type User = {
id: string;
name: string;
email: string;
avatar: string;
};But the user creation form doesn't need id.
Instead of creating another type:
type CreateUser = {
name: string;
email: string;
avatar: string;
};I can write:
type CreateUser = Omit<User, "id">;
The utility types I use most often:
Omit
To create variants of existing models:
type UpdateUser = Partial<Omit<User, "id">>;
Pick
When a component only needs part of the data:
type UserCardProps = Pick<User, "name" | "avatar">;
Record
For map and configurations:
type StatusLabels = Record<UserStatus, string>;
Utility Types make types evolve with the code.
3. Discriminated Unions: controlling application states
One of the most common problems in frontend development is state management.
For example, data retrieved from an API.
I often see code like this:
if (loading) return <Loader />
if (error) return <Error />
return <Data />The problem arises when the application grows.
A better approach is to describe all possible states:
type RequestState<T> =
| {
status: "loading";
}
| {
status: "success";
data: T;
}
| {
status: "error";
message: string;
};Now the component must handle each case.
In larger applications, especially in Next.js, this approach significantly reduces accidental errors.
4. Satisfies: configurations without loss of inference
satisfies is one of those features that's easy to miss at first.
Example from Next.js:
const navigation = {
home: "/",
blog: "/blog",
projects: "/projects",
} satisfies Record<string, string>;TypeScript will validate the structure but still preserve specific values.
This is very useful for:
- routing configurations,
- menus,
- metadata,
- permission maps,
- CMS configuration.
5. Infer: when TypeScript does the hard work for us
In React projects, we often use libraries with very complex types.
infer allows you to extract information from existing types.
Example:
type ApiResponse<T> = {
data: T;
error?: string;
};
type ExtractData<T> =
T extends ApiResponse<infer Data>
? Data
: never;TypeScript can now retrieve data types.
This is the foundation of many advanced utility types.
6. Template Literal Types: typing dynamic strings
Strings in apps are often not just random text.
Example:
type Event =
| "userCreated"
| "userDeleted";We can generate these types:
type Entity = "user" | "post";
type EventName =
`${Entity}Created`;Efekt:
"userCreated" | "postCreated"
It works great for:
- events,
- action names,
- design token systems,
- API integrations.
7. Type Guards: safe work with external data
Front-end applications often work with data that we cannot 100% trust.
API.
CMS.
Local Storage.
User input.
Example:
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"email" in value
);
}This allows us to safely narrow down the type.
This is especially important in applications based on:
- REST API,
- GraphQL,
- headless CMS.
8. keyof i typeof: types resulting from the code
One of the things I really like about TypeScript:
I don't have to write the same thing twice.
Example:
const permissions = {
admin: true,
editor: true,
viewer: false,
};
type Permission =
keyof typeof permissions;Now the type automatically updates with the object.
Less manual work.
Less room for error.
9. Readonly: fewer random mutations
In React, we often care about data immutability.
We can express this as follows:
type Config = {
readonly apiUrl: string;
};Attempt to change:
config.apiUrl = "/new-api";will be detected earlier.
This is particularly important when:
- application configurations,
- constants,
- shared data.
10. Strict Mode: the best TypeScript feature that's barely visible
Finally, something less glamorous, but very important.
{
"compilerOptions": {
"strict": true
}
}Strict mode forces you to write your code more mindfully.
Protects you from, among other things:
- accidental
undefined, - incomplete case handling,
- incorrect type assumptions.
Summary
In production projects, it's one of the first things I configure.
TypeScript has changed the way I design front-end applications.
The biggest change after several years of working with TypeScript?
I stopped treating types as something you add after writing the code.
Nowadays, I often start with the data model.
I wonder:
- What states are possible?
- What data should be required?
- Where should the component be flexible?
- Where should the API be limited?
Well-written types make refactoring a large application less risky.
What I value most about TypeScript isn't that it tells me where I'm going wrong, but that it helps me build a system that's harder to break.