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! 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; }

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!