How can I write clean, senior typescript code and avoid junior developer mistakes in Typescript? So in this particular article, I will go through 5 mistakes that every typescript developer makes and how you can avoid those mistakes to become a better developer.
Topics that I will cover in this article:
- Use unknown instead of any
- Use is operator
- Use satisfies operator
- Avoid using enums
- Use Typescript utility types
Use unknown instead of any
It's generally a good practice to use the unknown
type in TypeScript instead of the any
type whenever you don't have a specific type in mind for a variable or when you want to indicate that a variable can have a variety of different types.
Using the any
type means that you are telling the TypeScript compiler to completely ignore the type of a value, which can lead to unintended consequences and can make it difficult to catch type-related errors during development.
On the other hand, using the unknown
type tells the compiler that the type of a value is not known at the point where it is being declared, but that the type will be checked at runtime. This allows the compiler to catch type-related errors at development time, while still allowing you to use the value in a type-safe way.
For example, consider the following code:
function myFunction(fn: any) {
fn();
}
invokeAnything(1);
Because the fn param is of any
type, the statement fn() won't trigger type errors. You can do anything with a variable of type any.
But running the script throws a runtime error. Because one
is a number, not a function, TypeScript hasn't protected you from this error!
So, how to allow the myFunction()
function to accept any argument but force a type check on that argument? In that case, you can use the unknown
type.
Consider the following example:
function myFunction(fn: unknown) {
fn(); // triggers a type error
}
invokeAnything(1);
Here you will get a type error as you are using unkown
type. Contrary to any, TypeScript protects you now from invoking something that might not be a function!
So, to get rid of this problem you can perform type checking before using a variable of type unknown. In the example, you would need to check if fn
is a function type:
function myFunction(fn: unknown) {
if (typeof fn === 'function') {
fn(); // no type error
}
}
invokeAnything(1);
Now Typescript is happy as It knows that fn
will only be invoked when the parameter type will be a function.
Here's the rule that can help you to understand the difference:
- You can assign anything to an
unknown
type, but you have to do a type check to operate onunknown
- You can give anything to
any
type, and you can perform any operation onany
Use is operator
The second operator or keyword you are probably not using in your Typescript code is the is
operator.
Consider the following example:
type Species = "cat" | "dog";
interface Pet {
species: Species;
}
class Cat implements Pet {
public species: Species = "cat";
public meow(): void {
console.log("Meow");
}
}
function petIsCat(pet: Pet): pet is Cat {
return pet.species === "cat";
}
function petIsCatBoolean(pet: Pet): boolean {
return pet.species === "cat";
}
const p: Pet = new Cat();
In this example, I defined a type
Species
and an interface
called Pet
with that Species
type. Then I created a class
called Pet
that implements the Pet
interface and has a method called meow.
Later, I defined two type guards. And finally, a constant p
of type Pet
is defined and assigned a new instance of Cat
.
For those who don't know what type guard is, In TypeScript, a type guard
is a way to narrow the type of a variable within a conditional
block. It is a way to assert that a variable has a certain type, and the type checker will treat the variable as having that type within the block where the predicate is used.
In my example, The petIsCat()
function takes a parameter pet
of type Pet
and returns a type of pet is Cat
, which is a type guard that narrows the type of pet
to Cat
if the species property is equal to cat.
The petIsCatBoolean()
function is similar to petIsCat()
, but it returns a boolean value indicating whether the species property is equal to "cat" or not.
Ok, now let's see which type guard works better:
//Bad ❌
if (petIsCatBoolean(p)) {
p.meow(); // ERROR: Property 'meow' does not exist on type 'Pet'.
(p as Cat).meow();
//What if we have many properties? Do you wanna repeat the same casting
//Over and over again...
}
//Good ✅
if (petIsCat(p)) {
p.meow(); // now compiler knows for sure that the variable is of type Cat and it has meow method
}
In this example, there are two blocks of code that use the petIsCat()
and petIsCatBoolean()
functions to check whether the p
variable is of type Cat
. If it is, the code in the block can safely call the meow()
method on the p
variable.
The first block tries to call the meow
method but gets a type error. Because the meow
method is a method that is specific to the Cat
class, so it is not available on the Pet
interface.
However, because the p
variable is known to be of type Cat
due to the check performed by the petIsCatBoolean
function, the type assertion can be used to tell the TypeScript compiler that the p
variable should be treated as a Cat
within the block of code. This allows the meow
method to be called on the p
variable without causing a compile-time error.
But what if we have many properties? Do you wanna repeat the same casting? Of course no
.
So, now let's check the second if
block. The second block of code uses the petIsCat
function, which is a type guard, to check whether the p
variable is of type Cat. If it is, the type of the p
variable is narrowed to Cat
within the block of code. This means that the p
variable can be treated as a Cat
within the block, and the meow
method can be called directly on it without causing a compile-time error and casting any type.
As you can see the type guard with is
operator is way better than the one with only boolean type. That's why whenever you are trying to make a type guard, make sure to always use is
operator.
Use satisfies operator
In TypeScript, the satisfies
keyword is used to specify that a type or a value must conform to a given type or interface.
For instance, consider the following example:
type RGB = [red: number, green: number, blue: number];
type Color = { value: RGB | string };
const myColor: Color = { value: 'red' };
In this example, when you try to access myColor.value
, you will notice that you are not getting any string methods like toUpperCase()
in your auto recommendation. It's happening because Typescript is unsure if myColor.value
is a string
or RGB tuple.
But if you write the same code like this:
type RGB = [red: number, green: number, blue: number];
type Color = { value: RGB | string };
const myColor = { value: 'red' } satisfies Color;
Now by typing myColor.value
, you will get all of the string
methods in the auto recommendation. Because Typescript knows that myColor.value
is only a string,
not RGB tuple.
In the same way, if you had const myColor = { value: [255, 0, 0] } satisfies Color
, you would've gotten all of the array methods in your auto recommendation and not the string methods. Because Typescript knows that myColor.value
is only a RGB tuple,
not a string.
Avoid using enums
In Typescript try avoid enums
as much as possible because enums
can make your code less reliable and efficient.
Let's understand this with an example:
//Bad ❌
enum BadState {
InProgress,
Success,
Fail,
}
BadState.InProgress; // (enum member) State.InProgress = 0
BadState.Success; // (enum member) State.Success = 1
BadState.Fail; // (enum member) State.Fail = 2
const badCheckState = (state: BadState) => {
//
};
badCheckState(100);
//Good ✅
type GoodState = "InProgress" | "Success" | "Fail";
enum GoodState2 {
InProgress = "InProgress",
Success = "Success",
Fail = "Fail",
}
const goodCheckState = (state: GoodState2) => {};
goodCheckState("afjalfkj");
The code I provided has a couple of issues. First, in the BadState
enum, the values of the enum members are not specified, so they default to 0
, 1
, and 2
. This means that if you pass a number like 100
to the badCheckState
function, it will not cause a type error.
In contrast, in the GoodState
type, the only possible values are InProgress, Success
, and Fail
. This means that if you try to pass a value like afjalfkj
to the goodCheckState
function, it will cause a type error, because afjalfkj
is not a valid value for the GoodState
type.
The GoodState2
enum is similar to the GoodState
type, but it uses enum members instead of string literals. This means that you can use the dot
notation (e.g. GoodState2.InProgress) to access the enum members, which can make the code more readable in some cases. However, the values of the enum members in GoodState2
are specified as string literals, so they have the same type as the GoodState
type.
In general, it is usually a good idea to use a type (like GoodState
) or an enum (like GoodState2
) when you have a fixed set of possible values that a variable can take on. This can help prevent errors and make the code more maintainable.
Use Typescript utility types
Many utility
types are available in typescript, which can make life easier. But unfortunately, only a few people use them. So here I picked some of the most potent utility
types you can use whenever you want. These are: Partial
, Omit
and Record
Partial type
Partial
type in TypeScript allows you to make all properties of a type optional. It's useful when you want to specify that an object doesn't have to have all properties, but you still want to define the shape of the object.
For example, consider the following type Person:
type Person = {
name: string;
age: number;
occupation: string;
};
If you want to create a new type that represents a person, but where the age and occupation properties are optional, you can use the Partial
type like this:
type PersonPartial = Partial<Person>;
// This is equivalent to:
type PersonPartial = {
name?: string;
age?: number;
occupation?: string;
};
Omit type
In TypeScript, the Omit
type is a utility type that creates a new type by picking all properties of a given type and then removing some of them.
Here's an example of how you might use the Omit
type:
interface Person {
name: string;
age: number;
occupation: string;
}
type PersonWithoutAge = Omit<Person, 'age'>;
// The type PersonWithoutAge is equivalent to:
// {
// name: string;
// occupation: string;
// }
In this example, the Omit
type is used to create a new type PersonWithoutAge
that is identical to the Person
type, except that it does not have the age
property.
Record Type
In TypeScript, a Record
type is a way to define the shape of an object in which the keys are known and the values can be of any type. It is similar to a dictionary in other programming languages.
Here is an example of how you can define a record type in TypeScript:
type User = Record<string, string | number>;
const user: User = {
name: 'John',
age: 30,
email: 'john@example.com'
};
In this example, the User
type is a Record
type in which the keys are strings
and the values
can be either strings
or numbers
. The user object is then defined as an instance of the User
type and has three key-value pairs.
You can also use the Record
type to define a record type with a fixed set of keys and specific types for the values. For example:
type User = Record<'name' | 'age' | 'email', string | number>;
const user: User = {
name: 'John',
age: 30,
email: 'john@example.com'
};
In this case, the User
type is a record type with three fixed keys: name
, age
, and email
. The values for these keys must be strings
or numbers
.
Record
types can be useful when you want to define an object with a flexible set of keys, but you still want to enforce a certain structure for the values associated with those keys.
Conclusion
Overall, these are some mistakes that most typescript developers make. And I recommend avoiding these mistakes as It makes your code less reliable.
So, thank you, guys, for reading this article from start to end. I hope you enjoyed this typescript article and all the mistakes you should avoid to become a better developer. See you all in my next article.😊😊
Top comments (3)
This is a very good list of tips to become a better typescript dev. Very clear and concise examples too 👍
Thanks man, glad you liked it👌
You got a typo here