TypeScript Deep Dive: Mastering Types and Functions
Understanding and Using Types and Functions in TypeScript
Table of contents
- Prerequisites
- Types in TypeScript: The Building Blocks of Safety
- Primitive Types
- Arrays
- The 'Any' Type
- Implicit Any and How to Avoid It
- The 'Unknown' Type: A Safer Alternative to 'Any'
- Anonymous Types: Quick and Dirty Object Types
- Undefined and Optional Properties
- Optional Parameters
- Default Parameter Values
- Function Overloading: The Advanced Technique
- Type Annotations
- Variables
- Function Parameters
- Return Values
- Wrapping Up
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!