DEV Community

Cover image for 9 tricks that separate a pro Typescript developer from an noob 😎
Tapajyoti Bose
Tapajyoti Bose

Posted on

70 7 4 5 2

9 tricks that separate a pro Typescript developer from an noob 😎

Typescript is a must-know tool if you plan to master web development in 2025, regardless of whether it's for Frontend or Backend (or Fullstack). It's one of the best tools for avoiding the pitfalls of JavaScript, but it can be a bit overwhelming for beginners. Here are 9 tricks that will kick start your journey from a noob to a pro Typescript developer!

let's get started

1. Type inference

Unlike what most beginners think, you don't need to define types for everything explicitly. Typescript is smart enough to infer the data types if you help narrow it down.

// Basic cases
const a = false;  // auto-inferred to be a boolean
const b = true;   // auto-inferred to be a boolean
const c = a || b; // auto-inferred to be a boolean

// Niche cases in certain blocks
enum CounterActionType {
  Increment = "INCREMENT",
  IncrementBy = "INCREMENT_BY",
}

interface IncrementAction {
  type: CounterActionType.Increment;
}

interface IncrementByAction {
  type: CounterActionType.IncrementBy;
  payload: number;
}

type CounterAction = IncrementAction | IncrementByAction;

function reducer(state: number, action: CounterAction) {
  switch (action.type) {
    case CounterActionType.Increment:
      // TS infers that the action is IncrementAction
      // & has no payload when the code in this case is
      // being executed
      return state + 1;
    case CounterActionType.IncrementBy:
      // TS infers that the action is IncrementByAction
      // & has a number as a payload when the code in this
      // case is being executed
      return state + action.payload;
    default:
      return state;
 }
}
Enter fullscreen mode Exit fullscreen mode

2. Literal types

When you need a variable to hold specific values, the literal types come in handy. Consider you are building a native library that informs the user about the permissions status, you could implement it as:

type PermissionStatus = "granted" | "denied" | "undetermined";

const permissionStatus: PermissionStatus = "granted"; // ✅
const permissionStatus: PermissionStatus = "random";  // ❌
Enter fullscreen mode Exit fullscreen mode

Literal types are not only limited to strings, but work for numbers and booleans, too.

interface UnauthenticatedUser {
  isAuthenticated: false;
}

interface AuthenticatedUser {
  data: {
    /* ... */
 };
  isAuthenticated: true;
}

type User = UnauthenticatedUser | AuthenticatedUser;

const user: User = {
  isAuthenticated: false,
}; // ✅
Enter fullscreen mode Exit fullscreen mode

NOTE: To make realistic examples above, we have used Union with the literal types. You can use them as standalone types, too, but that makes them somewhat redundant (act as a type alias)

type UnauthenticatedUserData = null;
Enter fullscreen mode Exit fullscreen mode

3. Enums

As TypeScript docs define them:

Enums are one of the few features TypeScript has which is not a type-level extension of JavaScript.

Enums allow a developer to define a set of named constants. Using enums can make it easier to document intent or create a set of distinct cases. TypeScript provides both numeric and string-based enums.

You can define enums as follows:

enum PrivacyStatus {
  Public,
  Private,
}
Enter fullscreen mode Exit fullscreen mode

As an added functionality, you can also define string or number enums:

enum PrivacyStatus {
  Public = "public",
  Private = "private",
}
Enter fullscreen mode Exit fullscreen mode

By default (if unspecified), enums are mapped to numbers starting from 0. Taking the example above, the default enum values would be:

enum PrivacyStatus {
  Public,  // 0
  Private, // 1
}
Enter fullscreen mode Exit fullscreen mode

But if we introduce a new enum value, the values of the existing enums will change too.

enum PrivacyStatus {
  Public,     // 0
  OnlyWith,   // 1
  OnlyExcept, // 2
  Private,    // 3
}
Enter fullscreen mode Exit fullscreen mode

If you plan to store the enum values in a database, it's strongly recommended to define the values each enum should map to, instead of leaving them with the default numbers - else it's easy to run into a plethora of bugs when the list of values the enum contains changes.

4. Type guards

Type guards are a way to narrow down the type of a variable. They are functions that help you to check which type a value is at runtime.

// continuing from the previous example
interface UnauthenticatedUser {
  isAuthenticated: false;
}

interface AuthenticatedUser {
  data: {
    /* ... */
 };
  isAuthenticated: true;
}

const isAuthenticatedUser = (user: User): user is AuthenticatedUser =>
  user.isAuthenticated;

if (isAuthenticatedUser(user)) {
  // user is AuthenticatedUser & you can access the data property
  console.log(user.data);
}
Enter fullscreen mode Exit fullscreen mode

You can define custom type guards as per your requirement, but if you are lazy like me, try out @rnw-community/shared for some commonly used type guards such as isDefined, isEmptyString, isEmptyArray, etc

5. Index Signatures and Records

When you have dynamic keys in an object, you can use an index signature or Record to define its type:

enum PaticipationStatus {
  Joined = "JOINED",
  Left = "LEFT",
  Pending = "PENDING",
}

// Using index signature
interface ParticipantData {
 [id: string]: PaticipationStatus;
}

// Using Record
type ParticipantData = Record<string, PaticipationStatus>;

const participants: ParticipantData = {
  id1: PaticipationStatus.Joined,
  id2: PaticipationStatus.Left,
  id3: PaticipationStatus.Pending,
  // ...
};
Enter fullscreen mode Exit fullscreen mode

NOTE: In the code above, you should use either index signature or Record, not both - having both in will cause an error saying Duplicate identifier 'ParticipantData'

6. Generics

Occasionally, you may want to create a function or a class that is not restricted to work with multiple types of data. Generics allow you to handle just that. You can define a generic type by using angle brackets <>:

const getJsonString = <T>(data: T) => JSON.stringify(data, null, 2);

// usage
getJsonString({ name: "John Doe" }); // ✅
getJsonString([1, 2, 3]);            // ✅
getJsonString("Hello World");        // ✅
Enter fullscreen mode Exit fullscreen mode

Generics also allow you to define constraints on the type of data you want to work with. For example, if you want to create a function that only works with objects with ids, you can do it like this:

const removeItemFromArray = <T extends { id: string }>(
  array: T[],
  id: string
) => {
  const index = array.findIndex((item) => item.id === id);
  if (index !== -1) {
    array.splice(index, 1);
 }
  return array;
};

// usage
removeItemFromArray(
 [
  { id: "1", name: "John Doe" },
  { id: "2", name: "Jane Doe" },
 ],
 "1"
);                                   // ✅
removeItemFromArray([1, 2, 3], "1"); // ❌
Enter fullscreen mode Exit fullscreen mode

7. Immutable types

Immutable types are a way to ensure that the data in your objects or arrays cannot be modified, thus you can prevent unintended side effects and make your code predictable and easy to debug.

You can use Readonly and ReadonlyArray to enforce immutability.

Using Readonly for objects

interface User {
  name: string;
  age: number;
}

const user: Readonly<User> = {
  name: "John Doe",
  age: 30,
};

user.name = "Jane Doe"; // ❌ Error: Cannot assign to 'name' because it is a read-only property
Enter fullscreen mode Exit fullscreen mode

Using ReadonlyArray for arrays

const numbers: ReadonlyArray<number> = [1, 2, 3];

numbers.push(4); // ❌ Error: Property 'push' does not exist on type 'readonly number[]'
numbers[0] = 10; // ❌ Error: Index signature in type 'readonly number[]' only permits reading
Enter fullscreen mode Exit fullscreen mode

Deep immutability

If you need deep immutability (ensuring nested objects are also immutable), you can use libraries like deep-freeze or create custom utility types.

type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};

interface User {
  name: string;
  address: {
    city: string;
    country: string;
 };
}

const user: DeepReadonly<User> = {
  name: "John Doe",
  address: {
    city: "New York",
    country: "USA",
 },
};

user.address.city = "Los Angeles"; // ❌ Error: Cannot assign to 'city' because it is a read-only property
Enter fullscreen mode Exit fullscreen mode

You can start off by making the constant objects read only - there are hundreds of utilities for using the immutable pattern, eg: allows you to use time travel debugging in Redux, but they would require a lot deeper dive to explain in detail.

8. Utility types

Typescript introduces several utility types to generate types with niche characteristics using pre-existing types. These utility types are extremely useful when you need to generate a new type that is similar to an already existing type with minor alterations.

  1. Pick: Pick the necessary properties from an object type
  2. Omit: Pick all the properties from an object type, omitting the selected keys
  3. Partial: Make all properties of an object type optional
  4. Required: Make all properties of an object type required
interface User {
  name: string;
  age?: number;
  email: string;
}

type PickUser = Pick<User, "name" | "age">;
type OmitUser = Omit<User, "age">;
type PartialUser = Partial<User>;
type RequiredUser = Required<User>;

// PickUser is equivalent to:
// interface PickUser {
//   name: string;
//   age?: number;
// }

// OmitUser is equivalent to:
// interface OmitUser {
//   name: string;
//   email: string;
// }

// PartialUser is equivalent to:
// interface PartialUser {
//   name?: string;
//   age?: number;
//   email?: string;
// }

// RequiredUser is equivalent to:
// interface RequiredUser {
//   name: string;
//   age: number;
//   email: string;
// }
Enter fullscreen mode Exit fullscreen mode

9. Union & Intersection types

As already explored above, we can combine 2 or more types using Union types - Union types are defined using the | operator and combine multiple types into a single type. This is useful when you want to create a type that can be one of multiple different types.

interface UnauthenticatedUser {
  isAuthenticated: false;
}

interface AuthenticatedUser {
  data: {
    /* ... */
 };
  isAuthenticated: true;
}

// Union - user type allows both unauthenticated users and authenticated users to be stored
type User = UnauthenticatedUser | AuthenticatedUser;
Enter fullscreen mode Exit fullscreen mode

The intersection operator uses the & operator and combines the object types into a single type.

interface UserData {
  name: string;
  email: string;
  phoneNumber: string;
}

interface UserMetaData {
  lastSignedInAt: Date;
  signInLocation: {
    country: string;
    city: string;
 };
}

type UserFullData = UserData & UserMetaData;
// UserFullData is equivalent to:
// interface UserFullData {
//   name: string;
//   email: string;
//   phoneNumber: string;
//   lastSignedInAt: Date;
//   signInLocation: {
//     country: string;
//     city: string;
//   };
Enter fullscreen mode Exit fullscreen mode

Wrapping up

Now you know how pros use TypeScript! Give yourself a pat on the back.

pat on the back

That's all folks! 🎉

Thanks for reading

Need a Top Rated Software Development Freelancer to chop away your development woes? Contact me on Upwork

Want to see what I am working on? Check out my Personal Website and GitHub

Want to connect? Reach out to me on LinkedIn

Follow my blogs for bi-weekly new Tidbits on Medium

FAQ

These are a few commonly asked questions I receive. So, I hope this FAQ section solves your issues.

  1. I am a beginner, how should I learn Front-End Web Dev?
    Look into the following articles:

    1. Front End Buzz words
    2. Front End Development Roadmap
    3. Front End Project Ideas
    4. Transition from a Beginner to an Intermediate Frontend Developer
  2. Would you mentor me?

    Sorry, I am already under a heavy workload and do not have the time to mentor anyone.

ACI image

ACI.dev: The Only MCP Server Your AI Agents Need

ACI.dev’s open-source tool-use platform and Unified MCP Server turns 600+ functions into two simple MCP tools on one server—search and execute. Comes with multi-tenant auth and natural-language permission scopes. 100% open-source under Apache 2.0.

Star our GitHub!

Top comments (20)

Collapse
 
kc900201 profile image
KC

I've read from other articles that enums are to be avoided when it comes to data structuring in TypeScript. Lemme know if you have a different opinion. Overall, this is a well summarized reference and thanks for sharing.

Collapse
 
ruppysuppy profile image
Tapajyoti Bose

Yeah as mentioned in the article, enums may break pre-existing data in the DB if new values are introduced (if you don't explicitly mention the mapped values). If you plan to use enums, mapping them to a specific value is a non-negotiable, but you can always use constant objects, mapping the same values.

I personally prefer using enums & haven't run into any issues over the past 4 years apart from the unmapped enums being overwritten

Collapse
 
_ndeyefatoudiop profile image
Ndeye Fatou Diop

Great post! Clear and concise!
However I wouldn’t recommend enums since they have many issues. Best to combine an map with as const and create a type from it 🙏

Collapse
 
ruppysuppy profile image
Tapajyoti Bose • Edited

Check out the response to the other comment

Yeah as mentioned in the article, enums may break pre-existing data in the DB if new values are introduced (if you don't explicitly mention the mapped values). If you plan to use enums, mapping them to a specific value is a non-negotiable, but you can always use constant objects, mapping the same values.

I personally prefer using enums & haven't run into any issues over the past 4 years apart from the unmapped enums being overwritten

Collapse
 
_ndeyefatoudiop profile image
Ndeye Fatou Diop

So that is not the only issue with enums. The other thing with enums is that you cannot pass a string (which is the same value as the enum): which is super annoying.

enum Direction {
  Up = "up",
  Down = "down"
}

function move(direction: Direction) {
  console.log(direction);
}

move("down") // <- TS will complain here
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
ruppysuppy profile image
Tapajyoti Bose • Edited

You can write your own helper to convert the string to enum:

function toDirection(value: string): Direction {
  if (!Object.values(Direction).includes(value as Direction)) {
    throw Error("Invalid direction")
  }
  return value as Direction;
}
Enter fullscreen mode Exit fullscreen mode

Personally use zod to tackle the transformations & handle all Document/DTO schemas

Collapse
 
deividas_strole profile image
Deividas Strole

Fantastic article! It really highlights how powerful TypeScript is for catching errors early, improving code readability, and making large-scale projects easier to manage. Once you get used to it, it's hard to go back to plain JavaScript.

Collapse
 
hexshift profile image
HexShift

Nice! The tips on type inference and literal types are particularly handy, and I appreciate how you've broken things down into easily digestible bits that both newcomers and seasoned devs can benefit from. Shows just how much you can optimize your code with just a few key tricks. One suggestion I’d throw out there is a quick mention of using TypeScript with async/await patterns and sometimes the type inference can be a bit tricky there, so a little guidance would be useful.

Collapse
 
ruppysuppy profile image
Tapajyoti Bose

The only difference is when using async, the return type gets wrapped by a Promise

const asyncFn = async () => 0; // type AsyncFn = () => Promise<number>
Enter fullscreen mode Exit fullscreen mode

When you await, the Promise gets unwrapped & you can use it as a regular value

const func = async () => {
  const result = await asyncFn(); // type Result = number
}
Enter fullscreen mode Exit fullscreen mode

Everything else mentioned in the article still holds true regardless whether you are using Promises or not

Collapse
 
georgelakeg profile image
GeorgeLake

This was such a fun and insightful read! I love how the tips cut through the fluff and really emphasize habits that elevate code quality—like mastering utility types, using discriminated unions, and embracing type inference wisely. It's kind of like when you switch from watching random content to curated lists on PlayPelis APK—suddenly, the experience feels smarter and more intentional. Have you found one of these TypeScript tricks that gave you that big "aha!" moment in your own projects?

Collapse
 
ruppysuppy profile image
Tapajyoti Bose

Definitely literal types & using type guards - it insanely simplified handling types

Collapse
 
nevodavid profile image
Nevo David

Insane, this makes TypeScript feel way less scary tbh. The index signature vs Record part always trips me up lol.

Collapse
 
ecalcutin profile image
Evgenii Kalkutin

Are you even sleeping lol? I see you in almost every post. or you're bot haha?

Collapse
 
gajananpp profile image
Gajanan Patil

Nice article ! When I was learning Typescript, these Cheat Sheets on their website really helped me.

Collapse
 
nidomiro profile image
Niclas

Good list, although I do not use the TS-enum but other alternate structures for it.

Collapse
 
george_gardiakos_c9a7c2b7 profile image
George Gardiakos

This awesome. I use some of these but not all. Thank you!

Collapse
 
werliton profile image
Werliton Silva

I like it

Some comments may only be visible to logged-in visitors. Sign in to view all comments.

ACI image

ACI.dev: Fully Open-source AI Agent Tool-Use Infra (Composio Alternative)

100% open-source tool-use platform (backend, dev portal, integration library, SDK/MCP) that connects your AI agents to 600+ tools with multi-tenant auth, granular permissions, and access through direct function calling or a unified MCP server.

Check out our GitHub!