TypeScript Deep Dive: Mastering Types and Functions

TypeScript Deep Dive: Mastering Types and Functions

Understanding and Using Types and Functions in TypeScript

Welcome to Level 4 of our TypeScript adventure! In this lesson, we're going to explore the core of what makes TypeScript so powerful: its type system and how it interacts with functions. Buckle up, because we're about to level up our TypeScript skills!

Prerequisites

Before we dive in, make sure you're comfortable with JavaScript basics. If you need to set up your development environment, check out the first blog for the tools you'll need to install.

Types in TypeScript: The Building Blocks of Safety

TypeScript's type system is what sets it apart from JavaScript. Let's explore the various types you'll encounter:

Primitive Types

These are the basic building blocks:

const name: string = "Steve"; 
const health: number = 50;
const hardMode: boolean = false;
  • string: For text

  • number: For any numeric value

  • boolean: For true/false values

Arrays

Arrays in TypeScript can be typed too:

const names: string[] = ["Steve", "Bob"]; 
const health: number[] = [50, 100];
const hardMode: boolean[] = [false, true];

Using the same example above for primitive types we now have the same types but have assigned them as an Array with a list of values.

  • names is now an array of type string

  • health is an array of type number

  • hardMode is now an array of type boolean.

The 'Any' Type

The any type is TypeScript's escape hatch. It allows any value, but use it sparingly:

let name: any;
name = { first: "Steve", second: "The-Pirate" };
name = "Steve";
name = ["Steve"];
name = null;

Using any allows us to change the value and type of the variable name without TypeScript producing any type-checking errors. Ofcourse, this is not best practice to re-assign the value of name with different types but I just wanted to show you that we could when using the any type.

*Note: I would avoid using any where possible. Please create your own custom types if a standard type is not sufficient.

Implicit Any and How to Avoid It

TypeScript will infer any when it can't determine a type:

function throwDice(sides) {
  // 'sides' is implicitly 'any' as we haven't declared a type
}

If we have this in our TypeScript file, the variable sides will probably have a underline which if you hover over it in an IDE, it will give the following message:

(parameter) sides: any

We are not explicitly setting a type for sides. It should probably be a number, but TypeScript doesn’t have enough information to infer that. So its best guess is to use the type any.

Implicit any types are a common mistake when coming from JavaScript, so these should be used in very rare occasions.

Pro tip: Use "noImplicitAny": true in your tsconfig.json to catch these by default!

The 'Unknown' Type: A Safer Alternative to 'Any'

unknown is like any, but safer:

let name: unknown;
name.first = "Steve";
}

This will give us an error Object is of type 'unknown'.

This is happening because unknown is considered to be any possible type. It's different from any because you can't access any properties on it without narrowing it down first.

So to fix the example above we would have to narrow it down using the typeof command.

let name: unknown;
if (typeof name === "object" && name !== null && "first" in name) {
  name.first = "Steve";
}

If you didn't want to use the type unknown you would either have to create an interface or you can use an Anonymous type.

We will go into further detail on interfaces further on in this course.

Anonymous Types: Quick and Dirty Object Types

TypeScript can infer object types on the fly:

let name = { first: "Steve" };
console.log(name.first);

TypeScript infers the type of name based on the structure of the object literal. In this case, it infers the type { first: string }, which is an object type with a property first of type string.

We can access the first property of name using name.first without any type annotations or explicit type definitions.

By leveraging type inference, TypeScript can automatically determine the type of the object based on its structure. This allows you to write more concise code without explicitly specifying the type.

Undefined and Optional Properties

Make properties optional with a union type:

let name = { first: "Steve" as string | undefined };

name = {};
console.log(name.first);

In this case, we use a type assertion as string | undefined to explicitly specify that the first property can be either a string or undefined. This allows name to be assigned an object with or without the first property.

If you ran the code above, the first console.log would return Steve and the second console.log would return undefined and they would both be valid.

I do not want to go into too much detail of undefined types and I would recommend explicitly defining types in your code.Functions in TypeScript: Leveling Up Your Code

TypeScript adds some powerful features to JavaScript functions. Let's explore:

Optional Parameters

Make parameters optional with ?:

function createCharacter(name?: string, health?: number) {
  return {
    name: name ?? 'Steve',
    health: health ?? 100
  }
}
const player1 = createCharacter();
const player2 = createCharacter("Bob", 50);

In the example above, for variable player1 we call createCharacter() without any options and thanks to the optional parameters we set as part of the function, this will default to the name Steve and health 100.

For variable player2 we pass in values for the optional parameters which will then assign these values to the player2 variable. Which is currently set to Bob and 50.

Optional Parameters is not something that is only available in TypeScript. This can also be done in JavaScript, however with TypeScript we do get the benefit of Type-Checking. So if we decided to pass in a parameter that was of a different type, we would get an error from TypeScript before runtime.

Default Parameter Values

TypeScript can infer types from default values:

function createCharacter(name = 'Steve', health = 100) {
  return { name, health }
}

const player1 = createCharacter();
const player2 = createCharacter("Bob", 50);

In this example, we have put the default values as part of the functions parameters. So when we call the createCharacter() function for player1, this will automatically assign the values Steve and 100.

For player2, we still pass in values for both name and health and thanks to the way default parameter values work, we would re-assign those values to what we have passed into the function.

Function Overloading: The Advanced Technique

Function overloading allows multiple function signatures:
function createCharacter(): Character;
function createCharacter(health: number): Character;
function createCharacter(health: number, name: string): Character; 
function createCharacter(health?: number, name?: string): Character {
  return { 
    health: health ?? 100,
    name: name ?? 'Steve' 
  };
}

The function declarations are function overload signatures. They define different ways the createCharacter function can be called with additional parameters, which is sometimes known as overloading.

However, we have one function that has an implementation of the createCharacter function. It has optional parameters health and name with default values of 100 and Steve respectively.

The function creates a character object with the provided or default values and returns it.

The return type of this function implementation is Character, which is compatible with all the overload signatures.

The player1 variable will be of type Character with default values for health 100 and name Steve.

The player2 variable will be of type Character with the provided health value 10 passed into the function and the default value for name Steve.

The player3 variable will be of type Character with the provided health 50 and name Bob passed into the function.

Function overloading allows you to define multiple ways to call a function with different parameters added.

The TypeScript compiler uses the overload signatures to determine the appropriate types based on the arguments passed when the function is called.

The actual implementation of the function should be compatible with all the overload signatures.

Overloading provides flexibility in how a function can be called while maintaining type safety.

It allows you to define different parameter combinations and their corresponding return types, making the function more versatile and expressive.Type Annotations: Explicitly Defining Your Types

Type Annotations

TypeScript allows you to explicitly state types for:

Variables

const health: number; 
const name: string; 
const hardMode: boolean;

To specify the type of a variable, we must add the type after the variable declaration with a :, like we have already done many times in this lesson:

Function Parameters

function craftWeapon(materials: string[], skillLevel: number) {  
  // Function body
}

We can then call the function with the correct types as follows:

craftWeapon(["Iron", "Mithril"], 75);

If we did not pass the correct type into the function, TypeScript would help us realize we are using an argument with the wrong type with an error message.

Argument of type ‘string’ is not assignable to parameter of type 'number'

This would be the error if we passed a string into the skillLevel parameter instead of a number.

Return Values

function getHealth(): number {  
  return 50;
}
let health = getHealth();
console.log(typeof(health));
console.log(health);

Here, : number indicates that the getHealth function will return a value of type number. It's a type annotation that explicitly defines the expected return type of the function.

Using the typeof operator, this will tell us the type of the getHealth() function. This should return the type of number.

If we then console log health we should get the result 50.

Wrapping Up

Congratulations! You've just completed a deep dive into TypeScript's type system and function features. You've learned about:

  • Various types in TypeScript

  • How to use these types with variables and functions

  • Advanced features like function overloading

Remember, TypeScript's power comes from its ability to catch errors at compile-time. The more specific you are with your types, the more TypeScript can help you write bug-free code.

In the next level, we'll explore even more advanced TypeScript features. Keep coding, keep learning, and get ready for the next challenge!

Happy Coding!