In TypeScript, you can manipulate types in several ways. You can create new types from existing ones, combine types, and even create types that depend on other types.
You can create a new type using the type keyword. This is called a type alias. You can use type aliases to give a name to a type, which can be useful when you need to use the same type in multiple places. Think of it like a variable declaration for types.
type Age = number;
let age: Age = 30;
type Person = { name: string; age: Age };
let person: Person = { name: "Alice", age: 30 };You can combine types using the | and & operators. The | operator creates a union type, which means that a variable can hold a value of either type. The & operator creates an intersection type, which means that a variable must hold a value that satisfies both types.
// Union type
type streetNumber = string | number;
let street: streetNumber = "926B";
// Union type with literals
type Direction = "left" | "right" | "up" | "down" | number;
let up: Direction = "up";
let diagonal: Direction = 45;
// Intersection type
type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged & { alive: true }; // = { name: string, age: number, alive: true }
let alice: Person = { name: "Alice", age: 30, alive: true };TypeScript has a powerful type inference system that can narrow the type of a value based on the conditions it goes through. This is called type narrowing.
For example, TypeScript can infer the type of a value based on the type of a property:
type Person = { name?: string; age: number };
let data: Person | undefined = persons.find((p) => p.name === "Alice");
if (data !== undefined) {
// data has been narrowed down to the type Person
if(data.name !== undefined) {
// data.name has been narrowed down to the type string
console.log(data.name.toUpperCase());
}
}Sometimes, you know more about the type of a value than TypeScript does. It can happen when the value comes from an external source, like a user input or a third-party library, that you trust to send you a value of the proper type. Or it can be a shortcoming of TypeScript's type inference, which is really good but not perfect.
In such cases, you can use a type assertion to tell TypeScript the type of a value, using the as keyword.
let data = JSON.parse('{"name": "Alice", "age": 30}') as Person;
let age = document.querySelector("input#age").value as number;::: warning
This is forcing TypeScript to treat the value as the specified type, even if it's not. So this can be a source of bugs if you make a mistake. Use a type assertion only when there is no other way to tell TypeScript the type of a value.
:::
Type assertions are a way to directly tell TypeScript the type of a value. But what if the type of value is deduced by a function run at runtime for example? You can use a type predicate for that.
A type predicate is a function that checks the type of a value and returns a boolean. If the function returns true, TypeScript will narrow the type of the value inside the block where the predicate is called.
type Person = { name: string; age: number };
function isPerson(value: { [key: string]: any }): value is Person {
return typeof value.name === "string" && typeof value.age === "number";
}
let data = JSON.parse('{"name": "Alice", "age": 30}');
if (isPerson(data)) {
// can call toUpperCase() because data has been narrowed down to type Person
console.log(data.name.toUpperCase());
}You can use the keyof keyword to refer to the keys of an object type. This can be useful when you want to create a type that is a subset of the keys of another type.
type Person = { name: string; eyes: Color; mustache: boolean };
type PersonFeature = keyof Person; // = 'name' | 'eyes' | 'mustache'You may already know the JavaScript typeof operator which returns the type of a value:
typeof 42 === "number";
typeof "Alice" === "string";In TypeScript, you can use the typeof keyword in the context of a type annotation to refer to the type of a variable or a property.
let age = 30;
let ageType: typeof age; // = numberBy itself it is not very useful, but it can be used in combination with other features to create powerful types:
let person = { name: "Alice", eyes: "blue", mustache: false } as const;
type PersonFeature = keyof typeof person; // = 'name' | 'eyes' | 'mustache'You can use the [] operator to create a type that represents the type of a property of another type.
type Person = { name: string; age: number };
type Age = Person["age"]; // = number
type PersonField = Person[keyof Person]; // = string | number ; same as Person["age" | "name"]In the example above, we used literal types, but it works for any type:
const users: Person[] = [
{ name: "Alice", age: 30 },
{ name: "Bob", age: 25 },
];
type User = typeof users[number]; // = Person
type Length = typeof users["length"]; // = number
const userCollection: { size: number, [userId: string]: User } = {
alice: { name: "Alice", age: 30 },
bob: { name: "Bob", age: 25 },
};
type UserCollectionValue = userCollection[keyof typeof userCollection]; // = User | numberYou can create a type that depends on another type using conditional types. A conditional type is a type that is defined using a ternary operator.
type Person = { name: string; job: string };
type Animal = { name: string; legs: number };
let alice: Person = { name: "Alice", job: "developer" };
type AlicePronoun = typeof alice extends Person ? "she" | "he" : "it";
type FelixPronoun = typeof felix extends Animal ? "it" : "he" | "she";Note the use of the extends keyword. This is used to check if a type extends another type, i.e. if it would be valid if given this type as an annotation.
This can be combined with generics to create powerful types:
type Pronoun<T> = T extends Person ? "she" | "he" : "it";More on that in the next chapter: Generics.
Drag the blocks of text in the matching spots
Follow instructions in the following playground