DEV Community

Cover image for 5 Mistakes That Every Typescript Deverloper Should Avoid
Arafat
Arafat

Posted on • Edited on

5 Mistakes That Every Typescript Deverloper Should Avoid

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

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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 on unknown
  • You can give anything to any type, and you can perform any operation on any

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();
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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' };
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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;
// }
Enter fullscreen mode Exit fullscreen mode

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'
};
Enter fullscreen mode Exit fullscreen mode

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'
};
Enter fullscreen mode Exit fullscreen mode

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 (4)

Collapse
 
brense profile image
Rense Bakker

This is a very good list of tips to become a better typescript dev. Very clear and concise examples too 👍

Collapse
 
arafat4693 profile image
Arafat

Thanks man, glad you liked it👌

Collapse
 
kristiyan_velkov profile image
Kristiyan Velkov

Take your TypeScript skills to new heights with "Mastering TypeScript Core Utility Types":

📖 Buy on Leanpub
📖 Buy on Amazon

Collapse
 
tkhquang profile image
Quang Trinh

You got a typo here

Here you will get a type error as you are using unkown type.