Fundamental concepts to know as a Typescript beginner - Part 2

Fundamental concepts to know as a Typescript beginner - Part 2

These are the concepts I learned while doing the frontend masters' typescript course which helped me understand Typescript better.

Are you a beginner in TypeScript and looking to expand your knowledge?

Look no further!

In this article, we will be discussing the fundamental concepts of TypeScript that every beginner should know. From call signatures to non-null assertion operators, we will cover it all. By the end of this article, you will have a better understanding of TypeScript and be ready to take on more advanced topics.

So, let's dive in and expand our knowledge of TypeScript!

I assume that you have ready gone through the first part or know the basic concepts of typescript.

1. Call Signatures

We already know how to write type annotations for arguments and return types for a function. But how do we write callable types in interfaces and type aliases? By callable types, I mean callable functions that can be invoked. Up until now, we wrote functions like this with argument and return types.

const mutiple = (num1: number, num2: number): number => num1 * num2;

It is always necessary to annotate types in regular function declarations. We must include argument and return types because there's no way to infer them. If you compare with the below example, all the type information you need is on the left side of the assignment operator. This is called Call Signatures. It becomes much cleaner and more readable, especially when it comes to callbacks. We can define callable types both in interfaces and type aliases. As a result, we do not require type annotations for function arguments if we are using calling signatures.

type Multiple = (num1: number, num2: number) => number; // with types aliases

interface Multiple {
  ( num1: number, num2: number ): number;
} // with interfaces

const mutiple1: Multiple = (num1, num2) => num1 * num2;

2. Difference between void and undefined function return type

In Javascript, the function with no return statement returns a undefined value. TypeScript has a way of describing it with a void return type. In other words, the return type of this function should not be used; it should be ignored. Let's look at an example to understand better.

image.png

Here we are receiving an error since the push function returns a number. The difference between void and undefined return types is:

  • The return type of the example1 function says my return type should be ignored, which means even if the callback function returns something. It will be ignored and left unused.

  • In contrast, example2's return type says I will always return a undefined value. The return type of all functions that return no value is inferred to be void.

3. Construct Signatures

Contrary to call signatures, construct signatures describe what should happen with a new keyword. In TypeScript, these are called Construct signatures because they are used to create a new object.

interface DateConstructor {
  new (value: number): Date;
}

let MyDateConstructor: DateConstructor = Date;
const date1 = new MyDateConstructor();

Even though they are rare since we usually use classes that have all the functionalities built-in, you should be familiar with the construct signatures if you ever encounter them.

4. Function overloads

It is possible to overload functions in TypeScript. A function can have multiple parameters and return types with the same name. However, there should be a constant number of parameters.

It is necessary to define overload signatures and implementation signatures before we can put function overloading into practice.

There is nobody to the overload signature; it simply describes the parameters and returns of the function. In a function, multiple overload signatures may represent the different ways the function can be invoked.

On the other side, the implementation signature also contains the body that implements the function. We can have only one implementation signature.

image.png

The displayStudentDetails() function has 2 overload signatures and one implementation signature.

In each overload signature, one way in which the function can be invoked is described. In the case of the displayStudentDetails() function, you can call it in two ways: with a string argument or an array of strings argument.

Since the return type can be either string or an array of strings as per the first two function declarations, we must use compatible parameters and return type in the function definition.

5. Top types

Top types (symbol: ⊤) are types that describe anything i.e. any value allowable in JavaScript and that's what makes it a top type. It's the most flexible thing that exists in the type system. In TypeScript, there are two top types: any and unknown.

any

you can think of values with a any type as working with normal JavaScript.

let coupon: any = "CNVD"

It’s important to understand that any is not necessarily a problem. Sometimes, it’s exactly the right type to use for particular situations. For example,

image.png

If we look at the type of console.log(), it has a rest parameter. It takes any number of arguments you like, each of which is an any. This is appropriate because console.log() can log anything to the console. There's no reason to impose additional constraints here. It just indicates maximal flexibility.

unknown

like any, unknown can accept any values:

However, unknown is different from any in a very important way. In order to use values with unknown types, you first need to apply a type guard. Let's look at an example to understand better:

image.png

It can be seen that when we declare value1 with any type, there are no errors, even if we attempt to access a deep property. By contrast, value2 throws an error when accessing a deep property.

A value can be set to "unknown", but cannot be directly accessed. An unknown value can't be used unless it's narrowed. Unless you use a type guard with it to check it out, to make sure that it's acceptable for use. It's almost as if it comes with a warning label saying that you must verify that it's what you think it is before doing anything with it.

As we can see below, once we type guard the value2 it infers itself as string type.

image.png

6. Bottom Type

A bottom type (symbol: ⊥) is a type that describes no possible value allowed by the system. TypeScript provides one bottom type: never. But, why do we actually need them? Bottom types are used with exhaustive conditioning.

Let's understand what exhaustive conditions are.

Exhaustive conditionals

An example of an exhaustive conditional is in the default condition of the switch statement. In the below example, we have an exhaustive condition that handles every possibility of this course being one of a kind. The fact that we're saying this ends up as never that's another way of saying we've handled all of the cases.

image.png

Imagine a world where the stuff here, the kinds of courses, might be defined in some other file, maybe in a project with 10,000 JavaScript files. Someone alters this, and now all of the conditions, all of the different places and the rest of the code base, where they're looking through, they want to make sure they handle every case that this thing could be.

image.png

Now we see this conditional is no longer exhaustive. There is a new finance course which is launched. Therefore, we will get a meaningful error message if an unexpected value "slips through" until we run the code. Now you see all the friendly errors lit up, so you know that you need to go in and handle them all.

7. User-defined type guards

Real-world projects may require you to declare custom type guards to determine the types with custom logic. For example,

interface Course {
  name: string;
  type: string;
  seats: number;
}

let businessCourses: unknown;

if (
  businessCourses &&
  typeof businessCourses === "object" &&
  "name" in businessCourses &&
  typeof businessCourses["name"] === "string" &&
  "type" in businessCourses &&
  typeof businessCourses["type"] === "string" &&
  "seats" in businessCourses &&
  typeof businessCourses["seats"] === "number"
) {
  console.log(businessCourses); // type -> object
}

To determine whether the businessCourse is equivalent to the course type, we can use the if condition. But as you can see, over time, it will become messy.

So, the next step is to refactor it into a helper function, making it easier to call that in multiple files & making it less messy.

function isCourseType(valueToCheck: any) {
  return (
    valueToCheck &&
    typeof valueToCheck === "object" &&
    "name" in valueToCheck &&
    typeof valueToCheck["name"] === "string" &&
    "type" in valueToCheck &&
    typeof valueToCheck["type"] === "string" &&
    "seats" in valueToCheck &&
    typeof valueToCheck["seats"] === "number"
  );
}

if (isCourseType(businessCourse)) {
  businessCourse;
}

image.png

Even though we are using a type guard, the businessCourse infers to unknown 🙁. The problem here is that even though these are all type guards, there's nothing about isCourseType that says TypeScript to regard the true or false value that this function returns as an indication of the type of argument. TypeScript seems to have no idea that the return value isCourseType has anything to do with the type of valueToCheck.

The solution to this problem is user-defined type guards.

using is type guard

So we can say effectively that "Yes, isCourseTypeDefined returns a boolean." but that boolean should be taken as an indication of whether the value to test confirms the type Course. Now, if you see below, the type of businessCourse the if the condition is Course. It's only because we're telling typescript to trust us.

image.png

Note: The is type guard can be our best friend or it can be our worst enemy. Let's look at an example & understand better:

image.png

Here, even if we are not checking in the function whether the valueToCheck is of that particular type, we are still getting the someValue with no errors, and we can access the name, seats & type on it.

Type is only as good as your alignment between the actual checking logic you implement and your claim. If the logic contains any issues, we are going to run into big problems because typescript will do exactly what you say.

Right now, I'm saying everything is Course type. So, 89 apparently has a property called name on it. Typescript is doing what I'm telling it to do. So, these guards are the glue between compile-time validation and runtime behavior; we have to ensure that our compile-time validation and runtime behavior match up. And if they don't, we're lying to ourselves.

8. Non-null assertion operator

Using the non-null assertion operator (!.), we can eliminate the possibility of an undefined or null value. Despite the fact that this operator tells TypeScript to ignore null values and undefined values, the value could still be null or undefined.

image.png

Observe that line no. 11 throws a typescript error stating that the fruits object it food is undefined. This is true in this case. Using the non-null assertion operator on line no. 13, we say, "Typescript, I know that fruits will always be present in food object" which will eventually cause runtime errors.

A non-null assertion operator allows us to avoid unnecessary null and undefined checks. You shouldn't use this unless you are sure the variable or expression cannot be null. It is not recommended to use this in your app or library code.

That's all for today! Thank you very much for your patience.

These concepts are essential for those who want to expand their knowledge of TypeScript and be ready to take on more advanced topics. It is important to note that while some of these concepts can be powerful tools, they should be used with care and caution to ensure the code remains reliable and maintainable.

Let me know one new concept you learned from this blog that you weren't aware of.

I would love to know your answer and feedback :)

Did you find this article valuable?

Support Prerana Nawar by becoming a sponsor. Any amount is appreciated!