TypeScript: Creating Types from Types

Hello there, future TypeScript masters! I'm thrilled to be your guide on this exciting journey through the world of TypeScript types. As a computer science teacher with years of experience, I've seen firsthand how understanding types can transform a beginner into a coding wizard. So, let's embark on this adventure together!

TypeScript - Creating Types from Types

Union Types

Imagine you're at an ice cream parlor, and you can choose either chocolate or vanilla. In TypeScript, we call this kind of choice a "union type." It's like saying, "I want either this or that."

Let's dive into some code:

type Flavor = "chocolate" | "vanilla";

let myIceCream: Flavor;
myIceCream = "chocolate"; // This is okay
myIceCream = "strawberry"; // Error! Type '"strawberry"' is not assignable to type 'Flavor'.

In this example, Flavor is a union type that can be either "chocolate" or "vanilla". When we try to assign "strawberry" to myIceCream, TypeScript says, "Hold on! That's not on the menu!"

Union types are super helpful when you want to limit the possible values a variable can have. Here's another example:

type Answer = "yes" | "no" | "maybe";

function respondToQuestion(answer: Answer) {
    if (answer === "yes") {
        console.log("Great!");
    } else if (answer === "no") {
        console.log("Oh no!");
    } else {
        console.log("I see you're undecided.");
    }
}

respondToQuestion("yes"); // Works fine
respondToQuestion("perhaps"); // Error! Argument of type '"perhaps"' is not assignable to parameter of type 'Answer'.

Intersection Types

Now, let's talk about intersection types. If union types are about "this or that," intersection types are about "this and that." It's like ordering a combo meal at a fast-food restaurant - you get the burger and the fries and the drink.

Here's how it looks in code:

type Name = {
    firstName: string;
    lastName: string;
};

type Age = {
    age: number;
};

type Person = Name & Age;

let john: Person = {
    firstName: "John",
    lastName: "Doe",
    age: 30
};

In this example, Person is an intersection type that combines Name and Age. It's like saying, "A person must have a first name, a last name, and an age."

Here's another fun example:

type Swimmer = {
    swim: () => void;
};

type Flyer = {
    fly: () => void;
};

type FlyingFish = Swimmer & Flyer;

let nemo: FlyingFish = {
    swim: () => console.log("Swimming in the ocean"),
    fly: () => console.log("Flying over the waves")
};

nemo.swim(); // Output: Swimming in the ocean
nemo.fly();  // Output: Flying over the waves

Utility Types

TypeScript comes with a set of utility types that help us manipulate and transform existing types. They're like the Swiss Army knife of types! Let's look at some of the most commonly used ones:

Partial

Partial<T> makes all properties of a type optional. It's like saying, "I want all of these, but I don't need all of them right now."

interface Todo {
    title: string;
    description: string;
    completed: boolean;
}

function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
    return { ...todo, ...fieldsToUpdate };
}

const myTodo = {
    title: "Learn TypeScript",
    description: "Study for 2 hours",
    completed: false
};

const updatedTodo = updateTodo(myTodo, {
    description: "Study for 3 hours"
});

console.log(updatedTodo);
// Output: { title: "Learn TypeScript", description: "Study for 3 hours", completed: false }

Pick<T, K>

Pick<T, K> constructs a type by picking the set of properties K from T. It's like cherry-picking your favorite fruits from a fruit basket.

interface User {
    id: number;
    name: string;
    email: string;
    age: number;
}

type UserSummary = Pick<User, "id" | "name">;

const userSummary: UserSummary = {
    id: 1,
    name: "John Doe"
};

Omit<T, K>

Omit<T, K> constructs a type by picking all properties from T and then removing K. It's the opposite of Pick.

interface Product {
    id: number;
    name: string;
    price: number;
    description: string;
}

type ProductPreview = Omit<Product, "description">;

const productPreview: ProductPreview = {
    id: 1,
    name: "Cool Gadget",
    price: 99.99
};

Here's a table summarizing these utility types:

Utility Type Description
Partial Makes all properties in T optional
Pick<T, K> Constructs a type by picking the set of properties K from T
Omit<T, K> Constructs a type by picking all properties from T and then removing K

Typeof Type Operator

The typeof type operator in TypeScript allows us to create a type based on the shape of an existing value. It's like taking a mold of an object to create more objects with the same shape.

Let's look at an example:

const user = {
    name: "John",
    age: 30,
    location: "New York"
};

type User = typeof user;

const anotherUser: User = {
    name: "Jane",
    age: 25,
    location: "London"
};

In this example, we're using typeof user to create a new type User that matches the shape of our user object. This is incredibly useful when you want to ensure that multiple objects have the same structure.

Here's another example where typeof comes in handy:

const colors = ["red", "green", "blue"] as const;
type Color = typeof colors[number];

let myColor: Color = "red"; // OK
myColor = "yellow"; // Error: Type '"yellow"' is not assignable to type '"red" | "green" | "blue"'.

In this case, we're using typeof along with indexed access types to create a union type from an array of constants.

And there you have it, folks! We've journeyed through the land of TypeScript types, creating new types from existing ones. Remember, practice makes perfect, so don't be afraid to experiment with these concepts in your own projects. Happy coding, and may the types be ever in your favor!

Credits: Image by storyset