5 Mistakes That Every Typescript Deverloper Should Avoid
Tags: typescript, webdev, beginners, javascript
Published: 2022-12-27 16:00:49

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-unknown-instead-of-any)
- [Use **is** operator](#use-is-operator)
- [Use **satisfies** operator](#use-satisfies-operator)
- [Avoid using **enums**](#avoid-using-enums)
- [Use Typescript **utility** types](#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:

```ts
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:

```ts
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:

```ts
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 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:

```ts
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:

```ts
//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:

```ts
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:

```ts
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:

```ts
//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:

```ts
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:

```ts
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:

```ts
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:

```ts
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:

```ts
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.😊😊