Welcome back, future TypeScript master! In Chapter 1, we set up our development environment and got a taste of what TypeScript offers. Now, it’s time to dive into the heart of TypeScript: types.

This chapter is your foundational tour through the most common and essential data types that TypeScript provides. We’ll explore how to declare variables with specific types, understand why this is so powerful, and see how TypeScript helps you catch errors before your code even runs. Think of types as the blueprints for your data – they define what kind of information a variable can hold, making your code more predictable and robust.

By the end of this chapter, you’ll have a solid understanding of TypeScript’s basic types, including primitives like numbers, strings, and booleans, as well as more advanced concepts like arrays, tuples, and enums. This knowledge is crucial because types are the language TypeScript uses to understand and validate your code, leading to fewer bugs and a much smoother development experience. Ready to build a strong foundation? Let’s go!

What are Types, Anyway?

Before we jump into specific types, let’s quickly clarify what a “type” means in TypeScript. In JavaScript, variables can hold any kind of data – a number one moment, a string the next. This flexibility can sometimes lead to unexpected behavior or bugs that are hard to track down.

TypeScript introduces static typing, which means you (or TypeScript itself) declare the kind of data a variable is expected to hold. Once declared, that variable will only accept data of that specific type. If you try to assign something else, TypeScript will immediately tell you there’s a problem, often right in your code editor! This is like having a super-smart assistant constantly checking your work for consistency.

The Primitives: Your Everyday Types

Let’s start with the most fundamental building blocks – the primitive types. These are the simplest forms of data you’ll encounter.

The number Type

Just like in JavaScript, number in TypeScript covers both integers and floating-point numbers. There’s no separate int or float type; it’s all just number.

Why it matters: You’ll use numbers for calculations, IDs, quantities, and anything else involving numerical values. By explicitly typing something as a number, TypeScript ensures you don’t accidentally try to perform math on a string!

Let’s see it in action. Open your project from Chapter 1. Inside your src folder, create a new file named basic-types.ts.

Now, add the following code to src/basic-types.ts:

// src/basic-types.ts

// Declaring a variable 'age' and explicitly assigning it the 'number' type.
let age: number = 30;

// TypeScript can also *infer* the type if you assign a value immediately.
// Here, 'price' is inferred as 'number'.
let price = 19.99;

// Let's try to assign a non-number value (this will cause an error!)
// price = "expensive"; // Uncommenting this line would show a TypeScript error!

console.log(`Age: ${age}`);
console.log(`Price: ${price}`);

Explanation:

  • let age: number = 30;
    • let age: declares a variable named age.
    • : number is the type annotation. It tells TypeScript that age must be a number.
    • = 30; assigns an initial numerical value.
  • let price = 19.99;
    • Here, we didn’t explicitly add : number. TypeScript is smart! It sees that 19.99 is a number and infers that price should be of type number. This is a common and often preferred way to let TypeScript do the work for you when the type is obvious.
  • The commented-out line price = "expensive"; would cause a type error if uncommented. TypeScript would complain: “Type ‘string’ is not assignable to type ’number’.” This is TypeScript doing its job – protecting you from potential runtime errors!

To compile this, open your terminal in your project’s root directory and run:

npx tsc src/basic-types.ts

Then, run the compiled JavaScript:

node src/basic-types.js

You should see the output in your console. Notice how TypeScript caught the potential error before you even ran the code, if you tried to uncomment that line!

The string Type

The string type is used for text data. It can hold any sequence of characters, enclosed in single quotes ('), double quotes ("), or backticks (`) for template literals.

Why it matters: Strings are everywhere! Names, messages, URLs, file paths. Ensuring a variable is a string prevents accidental operations that only make sense for numbers or other types.

Let’s add some string examples to your src/basic-types.ts file, right below your number examples:

// src/basic-types.ts (continued)

// ... (previous number examples)

// Declaring a variable 'userName' with the 'string' type.
let userName: string = "Alice";

// Type inference works for strings too.
let greeting = `Hello, ${userName}!`; // This is a template literal.

// Let's try to assign a non-string value (another error!)
// userName = 123; // Uncommenting this would cause a TypeScript error!

console.log(`User Name: ${userName}`);
console.log(`Greeting: ${greeting}`);

Explanation:

  • let userName: string = "Alice"; explicitly types userName as a string.
  • let greeting = Hello, ${userName}!; infers greeting as a string because of the template literal. Template literals are a fantastic way to embed variables directly into strings in modern JavaScript and TypeScript.
  • Again, the commented-out line shows how TypeScript prevents you from assigning a number to a string variable.

Compile and run again to see the new output!

The boolean Type

The boolean type represents a logical entity and can have only two values: true or false.

Why it matters: Booleans are fundamental for conditional logic, flags, and switches in your application. Is a user logged in? Is an item available? These are boolean questions.

Add these boolean examples to src/basic-types.ts:

// src/basic-types.ts (continued)

// ... (previous number and string examples)

// Declaring a variable 'isActive' with the 'boolean' type.
let isActive: boolean = true;

// Type inference for booleans.
let hasPermission = false;

// Attempting to assign a non-boolean (you guessed it, an error!)
// isActive = "yes"; // Uncommenting this would show a TypeScript error!

console.log(`Is Active: ${isActive}`);
console.log(`Has Permission: ${hasPermission}`);

Explanation:

  • let isActive: boolean = true; explicitly types isActive as a boolean.
  • let hasPermission = false; infers hasPermission as a boolean.
  • The commented-out line demonstrates TypeScript’s type checking for booleans.

You’re doing great! These three (number, string, boolean) are your bread and butter.

Special Types: Handle with Care (or Wisdom!)

TypeScript also provides some special types that give you more flexibility, but some come with caveats.

The any Type (Use with Caution!)

The any type is a powerful escape hatch. When a variable is of type any, TypeScript essentially turns off all its type checking for that variable. It can hold any type of value, and you can access any properties on it, or call it as a function, without TypeScript complaining.

Why it matters: any can be useful when you’re migrating a JavaScript project to TypeScript, or when dealing with data from external sources where the type is truly unknown or constantly changing.

The Catch: Using any defeats the purpose of TypeScript! It removes type safety, making your code prone to the same kinds of errors you’d find in plain JavaScript. Use it sparingly and with awareness.

Let’s add an example of any to src/basic-types.ts:

// src/basic-types.ts (continued)

// ... (previous examples)

// The 'any' type: TypeScript turns off type checking.
let unknownValue: any = "Hello, world!";
console.log(`Any type (string): ${unknownValue}`);

unknownValue = 123; // No error! 'any' can be reassigned to any type.
console.log(`Any type (number): ${unknownValue}`);

unknownValue = { message: "I'm an object!" }; // No error!
console.log(`Any type (object): ${unknownValue.message}`);

// You can even call methods that might not exist at runtime!
// unknownValue.nonExistentMethod(); // TypeScript won't complain here, but it would crash at runtime!

Explanation:

  • let unknownValue: any = "Hello, world!"; declares unknownValue as any.
  • You can reassign unknownValue to a number, then an object, and TypeScript won’t bat an eye.
  • The commented-out line unknownValue.nonExistentMethod(); highlights the danger: TypeScript won’t warn you about potential runtime errors when using any. Be very careful!

The unknown Type (A Safer Alternative to any)

The unknown type was introduced in TypeScript 3.0 as a type-safe counterpart to any. Like any, a variable of type unknown can hold any value. However, unlike any, you cannot do anything with an unknown variable until you narrow down its type.

Why it matters: unknown forces you to perform type checks or assertions before you can use the value, making your code much safer and more predictable. It’s excellent for handling values from external APIs or user input where the type isn’t immediately certain.

Let’s replace our any example with unknown and see the difference:

// src/basic-types.ts (continued)

// ... (previous examples)

// The 'unknown' type: Safer than 'any'.
let potentiallyUnknown: unknown = "This could be anything.";
console.log(`Unknown type (string): ${potentiallyUnknown}`);

potentiallyUnknown = 42; // Still no error when assigning
console.log(`Unknown type (number): ${potentiallyUnknown}`);

// But you can't just use it without checking its type!
// let myString: string = potentiallyUnknown; // Error: Type 'unknown' is not assignable to type 'string'.

// To use 'potentiallyUnknown', you must first narrow its type.
if (typeof potentiallyUnknown === 'string') {
    let myString: string = potentiallyUnknown; // OK, now TypeScript knows it's a string!
    console.log(`After narrowing to string: ${myString.toUpperCase()}`);
}

if (typeof potentiallyUnknown === 'number') {
    let myNumber: number = potentiallyUnknown; // OK, now TypeScript knows it's a number!
    console.log(`After narrowing to number: ${myNumber * 2}`);
}

Explanation:

  • let potentiallyUnknown: unknown = "This could be anything."; declares potentiallyUnknown as unknown.
  • You can assign different types to potentiallyUnknown without error.
  • However, let myString: string = potentiallyUnknown; causes an error! TypeScript demands that you prove to it what the type is before it allows you to assign it to a more specific type.
  • The if (typeof potentiallyUnknown === 'string') block is a type guard. Inside this block, TypeScript is smart enough to know that potentiallyUnknown is now a string, allowing you to safely assign it to myString and use string methods like toUpperCase(). This is called type narrowing.

Always prefer unknown over any when you truly don’t know the type of a value. It’s a best practice for writing robust TypeScript code in 2025!

The void Type

The void type is used for functions that do not return any value. It’s essentially the opposite of any – it means “nothing here.”

Why it matters: It clearly communicates that a function’s purpose is to perform an action or have side effects, rather than to produce a result.

Let’s add a void function to src/basic-types.ts:

// src/basic-types.ts (continued)

// ... (previous examples)

// The 'void' type: For functions that don't return a value.
function logAction(message: string): void {
    console.log(`Action Log: ${message}`);
    // This function doesn't return anything.
    // return "something"; // Uncommenting this would cause an error!
}

logAction("User logged in.");

Explanation:

  • function logAction(message: string): void declares a function logAction that takes a string argument and explicitly states that it returns void.
  • If you tried to return a value from this function (like "something"), TypeScript would give you an error, enforcing the void return type.

The null and undefined Types

In JavaScript, null and undefined are distinct concepts, and TypeScript honors that.

  • undefined means a variable has been declared but has not yet been assigned a value.
  • null represents the intentional absence of any object value. It’s a value that you can explicitly assign to a variable to indicate that it currently holds no meaningful value.

Why it matters: Understanding null and undefined is crucial for handling missing data gracefully, especially when dealing with optional properties or values that might not always be present (e.g., a user’s middle name, or a database record that wasn’t found).

Important Note for 2025: Modern TypeScript projects almost always use strictNullChecks (which should be true in your tsconfig.json from Chapter 1). This setting means that null and undefined are not assignable to types like string or number by default. If you want a variable to potentially be null or undefined, you must explicitly include it in its type using a union type (which we’ll cover more later, but you’ll see a preview here).

Let’s add null and undefined examples to src/basic-types.ts:

// src/basic-types.ts (continued)

// ... (previous examples)

// 'undefined' for uninitialized variables.
let firstName: string = "Jane";
let middleName: string | undefined = undefined; // Explicitly allow undefined
// let middleName: string = undefined; // This would be an error with strictNullChecks!

console.log(`First Name: ${firstName}`);
console.log(`Middle Name: ${middleName}`); // Will print 'undefined'

// 'null' for intentional absence of value.
let userEmail: string | null = "[email protected]";
console.log(`User Email: ${userEmail}`);

userEmail = null; // We can explicitly set it to null
console.log(`User Email after clearing: ${userEmail}`);

// let lastName: string = null; // This would also be an error with strictNullChecks!

Explanation:

  • let middleName: string | undefined = undefined; Here, string | undefined is a union type. It means middleName can be either a string or undefined. This is how you tell TypeScript you expect a variable to sometimes not have a value.
  • Similarly, let userEmail: string | null = "[email protected]"; allows userEmail to be either a string or null.
  • The commented-out lines show errors that would occur if strictNullChecks is enabled (which it should be!). TypeScript forces you to be explicit about nullable types, which is a huge benefit.

Collection Types: Managing Groups of Data

Often, you’ll need to work with collections of data, not just single values. TypeScript provides powerful ways to type these collections.

Arrays (Type[] or Array<Type>)

Arrays are ordered lists of items. In TypeScript, you can specify that an array should only contain items of a certain type.

Why it matters: This ensures that every element in your list conforms to the expected structure, preventing errors when you iterate over the array or access its elements.

Let’s add array examples to src/basic-types.ts:

// src/basic-types.ts (continued)

// ... (previous examples)

// Arrays: A list of items of the same type.
// Syntax 1: Type[]
let numbers: number[] = [1, 2, 3, 4, 5];
console.log(`Numbers array: ${numbers}`);

// numbers.push("six"); // Error: Argument of type 'string' is not assignable to parameter of type 'number'.

// Syntax 2: Array<Type> (often used in more complex scenarios with generics)
let names: Array<string> = ["Alice", "Bob", "Charlie"];
console.log(`Names array: ${names}`);

// Accessing array elements
console.log(`First name: ${names[0]}`);

// An array of booleans, inferred type
let statusFlags = [true, false, true]; // Inferred as boolean[]
console.log(`Status Flags: ${statusFlags}`);

Explanation:

  • let numbers: number[] = [1, 2, 3, 4, 5]; declares numbers as an array where every element must be a number.
  • The commented-out numbers.push("six"); shows that TypeScript prevents you from adding a string to an array of numbers.
  • let names: Array<string> = ["Alice", "Bob", "Charlie"]; is an alternative syntax for declaring an array of strings. Both Type[] and Array<Type> are valid, with Type[] being more common for simple cases.
  • let statusFlags = [true, false, true]; demonstrates type inference for arrays. TypeScript sees all elements are booleans and infers statusFlags as boolean[].

Tuples

Tuples are a special type of array that allow you to express an array with a fixed number of elements, where each element has a known type. The order and number of elements are important.

Why it matters: Tuples are great for representing a fixed set of related values, like a coordinate pair [x, y], or a user’s [name, age, isAdmin] status. They enforce both the type and the order of elements.

Let’s add a tuple example to src/basic-types.ts:

// src/basic-types.ts (continued)

// ... (previous examples)

// Tuples: Fixed-size arrays where each element has a specific, known type.
let userProfile: [string, number, boolean] = ["David", 28, false];
console.log(`User Profile: Name: ${userProfile[0]}, Age: ${userProfile[1]}, Admin: ${userProfile[2]}`);

// userProfile = [29, "Eve", true]; // Error: Type 'number' is not assignable to type 'string'.
// userProfile = ["Frank", 35]; // Error: Source has 2 elements, but target requires 3.

// You can still use array methods on tuples, but be mindful of type safety.
userProfile.push("extra data"); // This is a known loophole in TS < 4.0, but still works.
// However, accessing userProfile[3] would be an error if you try to assign it to a specific type.
console.log(`User profile after push (be careful!): ${userProfile}`);

Explanation:

  • let userProfile: [string, number, boolean] = ["David", 28, false]; defines a tuple. It states that userProfile must be an array with exactly three elements: a string, then a number, then a boolean, in that exact order.
  • The commented-out lines show how TypeScript enforces both the type and the order/length of the tuple.
  • Important Note on push() with Tuples: While TypeScript typically enforces the fixed length of tuples, the push() method (and pop()) historically had a loophole where you could add elements beyond the declared tuple length. This is a subtle point and generally considered an edge case or a slight imperfection in the type system. For practical purposes, treat tuples as fixed-length. If you need dynamic length, use regular arrays.

Enums (Enumerations)

Enums allow you to define a set of named constants. This makes your code more readable and prevents typos when dealing with specific, limited sets of values.

Why it matters: Instead of using “magic numbers” or hardcoded strings (e.g., if (status === 0)), you can use descriptive names (e.g., if (status === OrderStatus.Pending)), which significantly improves code clarity and maintainability.

TypeScript supports both numeric and string-based enums.

Numeric Enums

By default, enums are numeric, starting their values from 0.

Add this to src/basic-types.ts:

// src/basic-types.ts (continued)

// ... (previous examples)

// Enums: A way to define a set of named constants.
// Numeric Enum (default behavior: starts from 0)
enum Direction {
    Up,      // 0
    Down,    // 1
    Left,    // 2
    Right    // 3
}

let playerDirection: Direction = Direction.Up;
console.log(`Player Direction (numeric value): ${playerDirection}`); // Output: 0
console.log(`Player Direction (name): ${Direction[playerDirection]}`); // Output: Up

// You can also manually assign values
enum StatusCode {
    Success = 200,
    NotFound = 404,
    ServerError = 500
}

let apiStatus: StatusCode = StatusCode.Success;
console.log(`API Status: ${apiStatus}`); // Output: 200

Explanation:

  • enum Direction { Up, Down, Left, Right } creates a numeric enum. Up automatically gets value 0, Down gets 1, and so on.
  • let playerDirection: Direction = Direction.Up; assigns the Up member to playerDirection. When logged, it shows its numeric value (0).
  • console.log(Direction[playerDirection]); shows how you can look up the name of an enum member from its value (this is called reverse mapping).
  • enum StatusCode { Success = 200, ... } shows how to manually assign numeric values. This is common when mapping to external codes (like HTTP status codes).

String Enums

String enums are often more readable and don’t suffer from reverse mapping issues. They are also generally preferred in modern TypeScript development (2025 best practice) because they provide better debugging messages and are less prone to accidental numeric comparisons.

Add this string enum example to src/basic-types.ts:

// src/basic-types.ts (continued)

// ... (previous examples)

// String Enum (more readable, often preferred)
enum UserRole {
    Admin = "ADMIN",
    Editor = "EDITOR",
    Viewer = "VIEWER"
}

let currentUserRole: UserRole = UserRole.Editor;
console.log(`Current User Role: ${currentUserRole}`); // Output: EDITOR

// if (currentUserRole === "ADMIN") { ... } // This works and is type-safe!

Explanation:

  • enum UserRole { Admin = "ADMIN", ... } creates a string enum. Each member is explicitly assigned a string literal.
  • When currentUserRole is logged, it outputs the string value ("EDITOR"), which is often more useful for debugging and logging than a number.
  • Comparing currentUserRole with "ADMIN" directly works because TypeScript understands the string literal types.

Great job getting through all the basic types! You’ve just learned the fundamental vocabulary of TypeScript.

Mini-Challenge: Your First Type-Safe Function!

Now it’s your turn to put some of these basic types into practice.

Challenge:

  1. Create a new file called challenge.ts in your src folder.
  2. Inside challenge.ts, define a string enum called ProductCategory with at least three categories (e.g., Electronics, Books, Clothing).
  3. Create a function named displayProductInfo. This function should accept the following arguments:
    • productName: a string
    • productId: a number
    • isAvailable: a boolean
    • categories: an array of ProductCategory (i.e., ProductCategory[])
    • price: a number | undefined (meaning the price might sometimes be missing)
  4. The function should console.log all the product information in a readable format.
  5. Call your displayProductInfo function twice:
    • Once with a product that has all information, including a price.
    • Once with a product that has no price (pass undefined for price).

Hint: Remember to use template literals (backticks `) for easy string formatting! For the price: number | undefined argument, you’ll need to check if price is actually a number before trying to display it, perhaps using an if statement.

What to Observe/Learn:

  • How TypeScript guides you to provide the correct types and number of arguments to your function.
  • How to handle optional parameters (number | undefined).
  • The clarity and self-documentation that enums bring.

Take your time, try it out, and don’t worry if you get stuck – that’s part of the learning process!


(Self-correction: The user will need to compile challenge.ts separately, or if they have a tsconfig.json set up for the entire src directory, then just npx tsc from the root would compile everything.)


Common Pitfalls & Troubleshooting

Even with basic types, there are a few common traps beginners fall into. Knowing them helps you avoid frustration!

  1. Over-reliance on any:

    • Pitfall: It’s tempting to use any when you’re not sure about a type or just want to quickly get rid of a TypeScript error. But any removes all type safety for that variable, defeating TypeScript’s purpose.
    • Solution: When you truly don’t know the type, use unknown instead of any. unknown forces you to perform type checks (typeof, instanceof) or type assertions (as Type) before you can use the value, making your code much safer.
    • Example:
      let dataFromApi: any = { name: "John", age: 30 };
      // dataFromApi.email.toLowerCase(); // No error from TS, but crashes at runtime!
      
      let saferDataFromApi: unknown = { name: "Jane", age: 25 };
      // saferDataFromApi.email.toLowerCase(); // Error: Object is of type 'unknown'.
      
      if (typeof saferDataFromApi === 'object' && saferDataFromApi !== null && 'name' in saferDataFromApi) {
          console.log((saferDataFromApi as { name: string }).name); // Type assertion after narrowing
      }
      
  2. Confusing null and undefined:

    • Pitfall: While both represent “no value,” their semantic meaning is different. Misunderstanding this can lead to incorrect logic or unexpected behavior, especially when dealing with optional data.
    • Solution: Remember: undefined means “not yet assigned” (or doesn’t exist); null means “intentionally absent.” Always use union types (e.g., string | null, number | undefined) when a value might legitimately be null or undefined, especially with strictNullChecks enabled.
    • Example:
      let optionalUser: { name: string; email?: string | null } = { name: "Max" };
      // Here, 'email' is optional (`?`) and can be either a string or null.
      // It's 'undefined' if not provided, and can be explicitly set to 'null'.
      
      if (optionalUser.email === undefined) {
          console.log("Email was not provided.");
      } else if (optionalUser.email === null) {
          console.log("Email was intentionally cleared.");
      } else {
          console.log(`User email: ${optionalUser.email}`);
      }
      
  3. Tuple vs. Array Misconceptions:

    • Pitfall: Thinking a tuple is just a “type-safe array” without appreciating its fixed-length, fixed-order nature.
    • Solution: Use arrays (Type[]) when you have a list of items of the same type and the length can vary. Use tuples ([Type1, Type2, Type3]) when you have a fixed number of elements, and each element has a specific type and position.
    • Example:
      let colors: string[] = ["red", "green", "blue"]; // Array: variable length, all strings.
      colors.push("yellow"); // OK
      
      let rgbColor: [number, number, number] = [255, 0, 128]; // Tuple: fixed length (3), fixed order/types.
      // rgbColor.push(0.5); // This technically works in TS, but conceptually breaks the tuple.
      // Always treat tuples as fixed-length for clarity and type safety.
      // let anotherRgb: [number, number, number] = [255, 0]; // Error: expects 3 elements.
      

By keeping these pitfalls in mind, you’ll write more robust and understandable TypeScript code.

Summary

Phew! You’ve just conquered the fundamental building blocks of TypeScript! Here’s a quick recap of what you’ve learned:

  • Primitive Types: number, string, boolean are your everyday types for basic data.
  • Type Inference: TypeScript is smart and can often figure out the type of a variable based on its initial value, saving you explicit annotations.
  • The any Type: A powerful escape hatch that disables type checking, but should be used very sparingly due to its lack of safety.
  • The unknown Type: A safer alternative to any, forcing you to narrow down the type before using it, promoting robust code.
  • The void Type: Used for functions that don’t return any value.
  • null and undefined: Representing intentional absence and uninitialized values, respectively. Remember to use union types (e.g., string | null) when these values are expected with strictNullChecks.
  • Arrays: Ordered lists of items, where all items must be of the same specified type (e.g., number[], Array<string>).
  • Tuples: Fixed-size arrays where each element has a specific, known type at a specific position (e.g., [string, number, boolean]).
  • Enums: A way to define a set of named constants, improving readability and preventing errors. Both numeric and string enums are available, with string enums often preferred for clarity.

You’ve built a strong foundation! In the next chapter, we’ll expand on these concepts by exploring how to define more complex data structures using objects, interfaces, and type aliases. Get ready to sculpt your data with even more precision!