DEV Community

Marouane Souda
Marouane Souda

Posted on

5 TypeScript Features You’re Probably Not Using (But Should Be)

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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)