Unveiling 7 expert TypeScript techniques pros utilize for enhanced productivity 🎩🔮
Embracing TypeScript can be a game-changer for coding efficiency and bug prevention. However, it can sometimes feel a bit daunting to use.
In this insightful piece, we explore 7 expert TypeScript techniques that can simplify your coding journey, as employed by seasoned professionals.
- Leveraging Type Inference The inherent intelligence of TypeScript enables it to infer data types when guided to do so.
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
return state + 1;
case CounterActionType.IncrementBy:
// TS infers that the action is IncrementByAction
// & has a number as a payload
return state + action.payload;
default:
return state;
}
}
In this scenario, TypeScript deduces the action’s type based on the type property, removing the need for you to verify whether a payload is present.
- Usage of Literal Types Literal types come in handy when specific variable values are required.
type Status = "idle" | "loading" | "success" | "error";
Literal types are not limited to strings; they work with numbers as well. In fact, you can further simplify things by using a map.
type Review = 1 | 2 | 3 | 4 | 5;
// or better yet:
const reviewMap = {
terrible: 1,
average: 2,
good: 3,
great: 4,
incredible: 5,
} as const;
// This will generate the same type as above,
// but it's much more maintainable
type Review = typeof reviewMap[keyof typeof reviewMap];
- Application of Type Guards Type guards offer another strategy to narrow down a variable’s type:
function isNumber(value: any): value is number {
return typeof value === "number";
}
const validateAge = (age: any) => {
if (isNumber(age)) {
// validation logic
// ...
} else {
console.error("The age must be a number");
}
};
Note: The example has been simplified for illustrative purposes. Ideally, it is preferable to use the specific type in your function, such as const validateAge = (age: number) => {...};
.
- Deploying Index Signature An index signature can be used to define the type when you’re dealing with dynamic keys in an object.
enum PaticipationStatus {
Joined = "JOINED",
Left = "LEFT",
Pending = "PENDING",
}
interface ParticipantData {
[id: string]: PaticipationStatus;
}
const participants: ParticipantData = {
id1: PaticipationStatus.Joined,
id2: PaticipationStatus.Left,
id3: PaticipationStatus.Pending,
// ...
};
- Implementing Generics Generics are a powerful way to enhance the reusability of your code. They enable you to set a type determined by the function usage.
const clone = <T>(object: T) => {
const clonedObject: T = JSON.parse(JSON.stringify(object));
return clonedObject;
};
const obj = {
a: 1,
b: {
c: 3,
},
};
const obj2 = clone(obj);
- Creating Immutable Types By adding
as const
, you can make your types immutable. This ensures accidental value changes are prevented.
const ErrorMessages = {
InvalidEmail: "Invalid email",
InvalidPassword: "Invalid password",
// ...
} as const;
// This will throw an error
ErrorMessages.InvalidEmail = "New error message";
Attempting to modify these immutable types will result in an error.
- Using Partial, Pick, Omit & Required Types While handling server and local data, it’s often necessary to make some properties optional or required.
interface User {
name: string;
age?: number;
email: string;
}
type PartialUser = Partial<User>;
type PickUser = Pick<User, "name" | "age">;
type OmitUser = Omit<User, "age">;
type RequiredUser = Required<User>;
// PartialUser is equivalent to:
// interface PartialUser {
// name?: string;
// age?: number;
// email?: string;
// }
// PickUser is equivalent to:
// interface PickUser {
// name: string;
// age?: number;
// }
// OmitUser is equivalent to:
// interface OmitUser {
// name: string;
// email: string;
// }
// RequiredUser is equivalent to:
// interface RequiredUser {
// name: string;
// age: number;
// email: string;
// }
You can also combine them using intersection: type A = B & C;
where B & C are any types.
That’s a wrap, folks! 🎉