TypeScript is an incredibly powerful language, but many developers are not taking advantage of its many rich features. I know because I used to be the same.
If you're only working with interface
, type
, and basic unions, you're missing out on options that can make your code cleaner, safer, and more maintainable.
Here are five underused TypeScript features that you need to be aware of. They'll make TypeScript development much more enjoyable once you learn them.
1. Discriminated Unions
This is my personal favourite feature of TypeScript. Discriminated unions are perfect for creating different states of a type with total type safety.
You define each object variant with a unique discriminant field (string literal), and TypeScript can then narrow the type for you in a switch or if-else block.
type Loading = { state: "loading" };
type Success = { state: "success"; data: string };
type Error = { state: "error"; message: string };
type FetchResult = Loading | Success | Error;
function handle(result: FetchResult) {
switch (result.state) {
case "loading":
console.log("Loading...");
break;
case "success":
console.log("Data:", result.data); // TypeScript knows `result.data` exists here
break;
case "error":
console.error("Error:", result.message); // TypeScript knows `result.message` exists here
break;
}
}
This lets you avoid if (x in obj)
hacks or unsafe any
types. Each case knows exactly what shape it's dealing with.
2. Template Literal Types
TypeScript lets you create literal string types dynamically using templates — just like template strings in JavaScript, but at the type level.
This is super useful for things like keys, paths, or enums.
type Lang = "en" | "es";
type Page = "home" | "about";
type Route = `${Lang}/${Page}`;
// Result: "en/home" | "en/about" | "es/home" | "es/about"
Now you can enforce valid combinations and get autocompletion, even for strings.
3. as const
for Literal Inference
By default, TypeScript widens object property types to general types like string
or number
.
const status = {
state: "success",
code: 200,
};
type StatusType = typeof status;
// { state: string; code: number }
Using as const
keeps the literals precise and makes the object readonly:
const status = {
state: "success",
code: 200,
} as const;
type StatusType = typeof status;
// { readonly state: "success"; readonly code: 200 }
This helps when you want exact types and avoid accidental mutation.
4. Indexed Access Types
Ever wish you could reuse a part of another type without retyping it manually?
That’s what indexed access types do.
type User = {
id: number;
profile: {
name: string;
age: number;
};
};
type UserName = User["profile"]["name"]; // string
This lets you pull out nested types from your existing structures. It’s safer than duplicating and makes refactoring much easier. You can combine this with generics for some powerful abstractions.
5. Key Remapping in Mapped Types
Want to dynamically rename keys or create variations of an existing type? You can use key remapping in mapped types.
type Original = {
name: string;
version: number;
};
type Prefixed<T> = {
[K in keyof T as `app_${string & K}`]: T[K];
};
type PrefixedKeys = Prefixed<Original>;
// Result: { app_name: string; app_version: number }
This is super helpful when you:
- Work with APIs where keys need prefixes/suffixes
- Want to transform types programmatically
- Need to namespace keys in shared types
Top comments (0)