You've got the basics of the type system in TypeScript; it's now time to see the overall picture it forms. The main logic behind the type system is straightforward: each type acts as a label for specific domains of values, whether those values are simple, complex or literal. These domains can cover finite or infinite value sets, depending on how you specify the type. Let's dig deeper to understand it better.
Regular data types
We all know the usual data types found across different programming languages, and they all exist in TypeScript: numbers, strings, booleans, arrays, objects, enums, tuples, and functions. These basic types form the core of TypeScript's type system. When we make type declarations for a variable using any of these keywords, we're defining a set with the given data type's name. In simple terms, it's a way of telling TypeScript that our variable can only take any value within the set and no other type.
Let's look at the straightforward practice in the following code where we define a variable called:
let userName: string;Here, we initialize the variable userName, annotated with the type string. As long as userName remains a string, or its value stays within the boundaries of the string set, we won't face any type errors, no matter the specific string values assigned to it:
userName = "LazyPanda"; // This is OK.
userName = "NinjaCoder34"; // This is OK too.
userName = ""; // This is OK; the value is an empty string.Just like the string example, the same rule applies to other built-in types in TypeScript. These usual types define the values a variable can hold, ensuring the variable remains within its assigned type.
The type 'any' and literal types
The any type is the broadest possible set of values in TypeScript. It's like a universal set in TypeScript types universe. In practice, this means that if you have a variable of type any, you can assign a value of any type to it, and TypeScript won't give you a type error. That means, it avoids TypeScript's type checking, so you should use it carefully.
Literal types, on the other hand, are a way to limit the possible values that a variable can take to a specific set of literal values. In this sense, a literal can be thought as a more precise sub-set of a collective set.
For example, if you write let status: "active";, then status can only ever be "active" and nothing else:
let status: "active";But, if you want to give status more options, you can use a union of literal types, like this:
let status: "active" | "loading" | "success" | "error";Now, status can be one of the four words: "active", "loading", "success", or "error". Here, you still have a limited number of choices, but more than one.
There's a significant difference here; a union of literal values lets status have a few specific choices. In contrast, literal types limit it to just one. On the other hand, the type any provides ultimate flexibility at the danger of errors, while literal types restrict a variable to a single value to maintain code integrity.
Union types and intersections
Union types in TypeScript allow a variable to be one of many types, but this also creates a finite set of types for the variable. In this set, all possible types are explicitly defined as parts of the union type.
For example, let's assume we have two interface-like types named User and Admin:
type User = {
userBadge: string;
};
type Admin = {
adminRank: number;
};If we want to create a new type that either has User's or Admin's features but avoids overlapping functionality, we create a union type. We use the pipe '|' operator to separate the constituents:
type Member = User | Admin;This way, we can create variables or parameters that can represent either a User or an Admin flexibly and safely, based on the situation.
In TypeScript, another method to merge different types into a single type is by using intersection types. This is a built-in TypeScript feature, which allows for specific and complex type definitions by combining varying types.
Intersection types are formed using the '&' operator, and the new type includes all the features of the combined types. Let's stick with the User and Admin types. To combine the properties of User and Admin, we create an intersection of them:
type Moderator = User & Admin;Then, based on this Moderator interface, we can create variables of Moderator type containing properties of both User and Admin:
let user4536: Moderator = {
name: "Alice",
adminLevel: 2
};You may notice, union types and intersection types seem to work similarly in some way; however, they have a key difference: Intersection types combine multiple types by including all their properties ('&', a 'logical AND'), whereas union types define a type that could be any one of several types ('|', a 'logical OR').
Note that union types and intersection types are complex topics which will be covered more comprehensively in separate topics. We're just introducing these concepts now so you can start understanding types as sets in TypeScript.
Exploring further: 'null', 'undefined', and 'never'
null, undefined, and never in TypeScript play specific roles, but they all share one thing: they are used where there are missing values, or unexpected behaviors. Thus, when considering these types as sets, we can think of null, undefined, and never as representing different kinds of "empty" sets in TypeScript, but with specific distinctions:
nullis like an empty set that you've purposely made empty. It indicates a deliberate lack of any object value.
let optionalName: string | null = null;
// `optionalName` can either belong to the string set or be explicitly emptyundefinedis like an empty set because you haven't filled it yet. It's used when a variable is declared but hasn't yet been given a value.
let pendingTask: string | undefined;
// `pendingTask` is like an empty slot, either to be filled with a string or left undefined.neveris a bit different; it's more than just an empty set: it's used to represent types that can never occur.
function failWithError(errorMessage: string): never {
throw new Error(errorMessage);
// `failWithError` ends the function, and it never returns anything.
}Since null and undefined are actual values that you can assign to a variable, that's why they are considered as 'assignable empty sets'. But, never is a type representing an empty set as it contains no values, but can't be assigned to a variable because it's not an actual value.
Conclusion
In TypeScript, it helps to picture types as clusters of values, also known as "sets". Elementary types like numbers, strings, and booleans can be seen as basic sets. More complex types, like union and intersection types, let us blend these fundamental sets in several ways to make new ones. Similarly, unique types such as null, undefined, and never can be imagined as empty sets, each carrying distinct significance. By grasping these types, you can create code that's clearer to understand and less likely to have errors.