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!
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;
}
}
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"; // ❌
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,
}; // ✅
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;
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,
}
As an added functionality, you can also define string or number enums
:
enum PrivacyStatus {
Public = "public",
Private = "private",
}
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
}
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
}
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);
}
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,
// ...
};
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"); // ✅
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"); // ❌
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
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
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
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.
-
Pick
: Pick the necessary properties from an object type -
Omit
: Pick all the properties from an object type, omitting the selected keys -
Partial
: Make all properties of an object type optional -
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;
// }
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;
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;
// };
Wrapping up
Now you know how pros use TypeScript! Give yourself a 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.
-
I am a beginner, how should I learn Front-End Web Dev?
Look into the following articles: Would you mentor me?
Sorry, I am already under a heavy workload and do not have the time to mentor anyone.
Top comments (20)
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.
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 useenums
, 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 overwrittenGreat 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 🙏
Check out the response to the other comment
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.
You can write your own helper to convert the string to enum:
Personally use zod to tackle the transformations & handle all Document/DTO schemas
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.
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.
The only difference is when using
async
, the return type gets wrapped by aPromise
When you
await
, thePromise
gets unwrapped & you can use it as a regular valueEverything else mentioned in the article still holds true regardless whether you are using
Promises
or notThis 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?
Definitely literal types & using type guards - it insanely simplified handling types
Insane, this makes TypeScript feel way less scary tbh. The index signature vs Record part always trips me up lol.
Are you even sleeping lol? I see you in almost every post. or you're bot haha?
Nice article ! When I was learning Typescript, these Cheat Sheets on their website really helped me.
Good list, although I do not use the TS-enum but other alternate structures for it.
This awesome. I use some of these but not all. Thank you!
I like it
Some comments may only be visible to logged-in visitors. Sign in to view all comments.