Skip to main content

type-guards-and-differentiating-types

Understanding Type Guards:

Type Guards are like detective tools in TypeScript, helping you to determine the specific type of a variable. It's like using clues to figure out a mystery.

function isString(test: any): test is string {
return typeof test === "string";
}

let value: any = "I am a string";

if (isString(value)) {
// value is now of type string
console.log(value.substring(0, 5)); // Output: "I am "
}

The 'typeof' Guard:

The 'typeof' operator is used to check simple data types. It's akin to identifying whether something is a fruit or a vegetable by its characteristics.

function padLeft(value: string, padding: string | number) {
if (typeof padding === "number") {
return Array(padding + 1).join(" ") + value;
}
if (typeof padding === "string") {
return padding + value;
}
throw new Error(`Expected string or number, got '${typeof padding}'.`);
}

The 'instanceof' Guard:

Use 'instanceof' to confirm an object's blueprint, like matching a pet to its breed at a dog show.

class Bird {
fly() {
console.log("Flying");
}
}

class Fish {
swim() {
console.log("Swimming");
}
}

function move(pet: Bird | Fish) {
if (pet instanceof Bird) {
pet.fly();
} else {
pet.swim();
}
}

const myPet = new Bird();
move(myPet); // Calls myPet.fly()

Custom Type Guards:

You can craft your own type guards to check for unique conditions, similar to creating a custom test that only one kind of item can pass.

interface Cat {
meow: () => void;
}

interface Dog {
woof: () => void;
}

function isCat(pet: Cat | Dog): pet is Cat {
return (pet as Cat).meow !== undefined;
}

function makeSound(pet: Cat | Dog): void {
if (isCat(pet)) {
pet.meow();
} else {
pet.woof();
}
}

Type Guard with 'in' Operator:

The 'in' operator checks if certain properties exist on an object, just as you might check for specific features like a camera on a phone.

interface House {
address: string;
}

interface Boat {
anchor: boolean;
}

function getLocation(thing: House | Boat) {
if ("anchor" in thing) {
// thing is treated as Boat
console.log("Boat location");
} else {
// thing is treated as House
console.log("House location");
}
}

Discriminated Unions:

This technique labels each member of a union type, helping you differentiate them like color-coded keys for different locks.

interface Circle {
kind: "circle";
radius: number;
}

interface Square {
kind: "square";
sideLength: number;
}

type Shape = Circle | Square;

function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
}
}

Narrowing with Control Flow Analysis:

TypeScript uses the direction of your code to figure out types, like a story that changes based on your decisions at key points.

function doSomething(x: number | string) {
if (typeof x === "string") {
console.log(x.substr(1)); // x is treated as a string here
} else {
console.log(x.toFixed(2)); // x is treated as a number here
}
}

The Non-null Assertion Operator:

By using this operator, you're telling TypeScript that a value is definitely not null, much like crossing your fingers for good weather.

function myFunction(elementId: string) {
// The '!' non-null assertion operator is used to assert that 'document.getElementById'
// will not return null, so TypeScript allows the use of 'textContent'.
const element = document.getElementById(elementId)!;
element.textContent = "Hello, World!";
}

Type Assertions:

Type assertions let you inform TypeScript about the specific type of an object, like recognizing your own pet in a crowd of animals.

let someValue: unknown = "This is a string";

// By using 'as string', you're telling TypeScript that you know 'someValue' is a string,
// even though its type is 'unknown'.
let strLength: number = (someValue as string).length;

Nullish Coalescing:

This provides a default value for null or undefined inputs, similar to having a backup plan in case your first option falls through.

let myVar: string | null = null;

// If 'myVar' is null or undefined, 'defaultString' will be used instead.
let result = myVar ?? "defaultString";

Optional Chaining:

Optional chaining allows you to safely navigate through potentially undefined properties, like using a gadget that automatically adjusts to perform different tasks.

interface User {
info?: {
email?: string;
};
}

const user: User = {};

// If 'user.info' is undefined, 'email' will be undefined instead of causing an error.
let email = user.info?.email;

Union Types:

Union types let a variable be one of several types, like a multi-purpose tool.

function printId(id: number | string) {
console.log("Your ID is: " + id);
}

// Both calls are valid
printId(101); // Your ID is: 101
printId("202"); // Your ID is: 202

Intersection Types:

Intersection types combine types, similar to two-in-one products that serve dual purposes.

interface BusinessPartner {
name: string;
credit: number;
}

interface Identity {
id: number;
}

type Employee = BusinessPartner & Identity;

let worker: Employee = {
name: "John Doe",
credit: 100,
id: 1
};

Aliases and Interfaces:

These let you define custom types with a specific name, like creating a specialized term for a concept.

type Point = {
x: number;
y: number;
};

interface Serializable {
serialize(): string;
}

Using Type Guards in Function Overloads:

You can use type guards in function overloads to make a function act differently based on the type of argument, like a machine that changes function based on which button you push.

function getValue(value: string): string;
function getValue(value: number): number;
function getValue(value: string | number): string | number {
if (typeof value === "string") {
return `My string: ${value}`;
} else {
return `My number: ${value}`;
}
}

console.log(getValue("test")); // Output: My string: test
console.log(getValue(123)); // Output: My number: 123

These code samples provide concise examples of each TypeScript feature, illustrating how they can be used to write safer and more readable code.