TypeScript

Basic Types in TypeScript

22 min Lesson 3 of 40

Understanding TypeScript's Type System

TypeScript's type system is what sets it apart from JavaScript. Types allow you to define the shape of your data, catch errors early, and make your code self-documenting. In this lesson, we'll explore the basic types that form the foundation of TypeScript's type system.

Type Annotations: In TypeScript, you add type information using a colon followed by the type name: let variableName: typeName = value;

Primitive Types

string

The string type represents textual data. You can use single quotes, double quotes, or template literals:

let firstName: string = "John";
let lastName: string = 'Doe';
let fullName: string = `${firstName} ${lastName}`;

// Type inference - TypeScript can infer the type
let city = "New York";  // Type is inferred as string

// Error examples
let age: string = 25;  // Error: Type 'number' is not assignable to type 'string'

number

The number type represents both integers and floating-point numbers. TypeScript, like JavaScript, doesn't distinguish between different numeric types:

let age: number = 30;
let price: number = 99.99;
let hex: number = 0xf00d;       // Hexadecimal
let binary: number = 0b1010;    // Binary
let octal: number = 0o744;      // Octal

// Special numeric values
let notANumber: number = NaN;
let infinite: number = Infinity;
let negInfinite: number = -Infinity;

// Numeric separators for readability (ES2020+)
let million: number = 1_000_000;
let billion: number = 1_000_000_000;

boolean

The boolean type has only two values: true and false:

let isActive: boolean = true;
let hasPermission: boolean = false;

// Boolean expressions
let isAdult: boolean = age >= 18;
let canVote: boolean = isAdult && hasPermission;

// Error example
let isValid: boolean = "true";  // Error: Type 'string' is not assignable to type 'boolean'

Type Inference: TypeScript is smart enough to infer types in many cases. If you initialize a variable with a value, you often don't need to explicitly declare the type: let name = "John"; automatically gets type string.

Special Types

any

The any type is an escape hatch that turns off type checking. A variable of type any can hold any value and you can perform any operation on it:

let data: any = "hello";
data = 42;              // OK
data = true;            // OK
data = { x: 10 };       // OK

// Can call any method without type checking
data.toUpperCase();     // No error, even if data isn't a string
data.nonExistent();     // No error at compile time

// any is contagious - operations with any return any
let result = data + 5;  // result is also type any

Avoid any When Possible: Using any defeats the purpose of TypeScript. It should only be used when migrating JavaScript code to TypeScript or when dealing with truly dynamic data where the type cannot be known. Even then, consider using unknown instead.

unknown

The unknown type is a type-safe alternative to any. Like any, a variable of type unknown can hold any value, but you must perform type checking before using it:

let userInput: unknown = getUserInput();

// Cannot use unknown values without type checking
userInput.toUpperCase();  // Error: Object is of type 'unknown'

// Must check the type first
if (typeof userInput === "string") {
    console.log(userInput.toUpperCase());  // OK - TypeScript knows it's a string
}

// Type assertions (use with caution)
let strInput = userInput as string;

// Better: Type guard function
function isString(value: unknown): value is string {
    return typeof value === "string";
}

if (isString(userInput)) {
    console.log(userInput.toUpperCase());  // OK
}

Best Practice: Use unknown instead of any when the type is truly unknown. This forces you to perform type checking, making your code safer.

void

The void type represents the absence of a value. It's commonly used as the return type of functions that don't return anything:

function logMessage(message: string): void {
    console.log(message);
    // No return statement
}

function saveData(data: any): void {
    localStorage.setItem("data", JSON.stringify(data));
    return;  // OK - can return without a value
}

// Rarely used for variables
let unusable: void = undefined;  // OK
let alsoUnusable: void = null;   // OK only with --strictNullChecks off

Note: A function that returns void can still execute a return statement, but it cannot return a value. This is different from functions that return undefined.

null and undefined

TypeScript has two types for representing absence: null and undefined:

let nothing: null = null;
let notDefined: undefined = undefined;

// With strictNullChecks: false (not recommended)
let name: string = null;       // OK
let age: number = undefined;   // OK

// With strictNullChecks: true (recommended)
let name: string = null;       // Error
let age: number = undefined;   // Error

// Explicitly allow null/undefined
let name: string | null = null;              // OK
let age: number | undefined = undefined;     // OK
let data: string | null | undefined = null;  // OK

Recommendation: Always enable strictNullChecks in your tsconfig.json. This forces you to explicitly handle null and undefined, preventing many runtime errors.

never

The never type represents values that never occur. It's used for functions that never return (they throw errors or have infinite loops):

// Function that throws an error
function throwError(message: string): never {
    throw new Error(message);
}

// Function with infinite loop
function infiniteLoop(): never {
    while (true) {
        // This never ends
    }
}

// never is the return type of functions that can't complete
function fail(msg: string): never {
    throw new Error(msg);
}

// never is useful in exhaustive type checking
type Shape = "circle" | "square";

function getArea(shape: Shape): number {
    switch (shape) {
        case "circle":
            return Math.PI * 10 * 10;
        case "square":
            return 10 * 10;
        default:
            // If we add a new shape and forget to handle it,
            // TypeScript will error here
            const exhaustiveCheck: never = shape;
            return exhaustiveCheck;
    }
}

Understanding never: The never type is the bottom type in TypeScript's type system. It's assignable to every type, but no type (except never itself) is assignable to never. This makes it useful for exhaustiveness checking.

Type Inference and Type Annotations

When to Use Type Annotations

TypeScript can often infer types automatically, but there are cases where you should explicitly annotate types:

// Type inference works well
let name = "John";           // inferred as string
let age = 30;                // inferred as number
let isActive = true;         // inferred as boolean

// When inference doesn't work or isn't clear
let numbers;                 // inferred as any - BAD
let numbers: number[];       // explicitly typed - GOOD

// Function parameters always need types
function greet(name: string) {  // parameter must be typed
    return `Hello, ${name}`;    // return type inferred as string
}

// Sometimes explicit return types improve readability
function add(a: number, b: number): number {
    return a + b;
}

Type Widening and Narrowing

TypeScript can widen or narrow types based on context:

// Type widening
let x = "hello";     // Type is 'string', not literal "hello"
const y = "hello";   // Type is literal "hello" (const can't be reassigned)

// Type narrowing with type guards
function processValue(value: string | number) {
    // value is string | number here

    if (typeof value === "string") {
        // value is narrowed to string here
        console.log(value.toUpperCase());
    } else {
        // value is narrowed to number here
        console.log(value.toFixed(2));
    }
}

// Truthiness narrowing
function printLength(str: string | null) {
    if (str) {
        // str is narrowed to string (not null)
        console.log(str.length);
    } else {
        console.log("No string provided");
    }
}

Type Assertions

Sometimes you know more about a type than TypeScript does. Type assertions let you override TypeScript's inference:

// Angle-bracket syntax (not usable in JSX/TSX files)
let someValue: unknown = "this is a string";
let strLength: number = (<string>someValue).length;

// as syntax (preferred, works everywhere)
let someValue: unknown = "this is a string";
let strLength: number = (someValue as string).length;

// Real-world example: DOM manipulation
const canvas = document.getElementById("myCanvas");  // Type: HTMLElement | null
const ctx = (canvas as HTMLCanvasElement).getContext("2d");

// Double assertion (avoid unless absolutely necessary)
const value = "hello" as any as number;  // BAD - defeats type safety

Use Type Assertions Carefully: Type assertions bypass TypeScript's type checking. Use them only when you're certain about the type. Prefer type guards when possible.

Literal Types

Literal types let you specify exact values that a variable can have:

// String literals
let direction: "up" | "down" | "left" | "right";
direction = "up";      // OK
direction = "north";   // Error

// Numeric literals
let diceRoll: 1 | 2 | 3 | 4 | 5 | 6;
diceRoll = 3;   // OK
diceRoll = 7;   // Error

// Boolean literals (less common)
let isTrue: true = true;
let isFalse: false = false;

// Combining with union types
type Status = "success" | "error" | "pending";
type Level = 1 | 2 | 3 | 4 | 5;

function setStatus(status: Status): void {
    console.log(`Status: ${status}`);
}

setStatus("success");  // OK
setStatus("failed");   // Error

Type Aliases

Type aliases create new names for types, making your code more readable:

// Basic type alias
type UserID = string | number;

let id1: UserID = "abc123";
let id2: UserID = 456;

// Complex type alias
type Point = {
    x: number;
    y: number;
};

let point: Point = { x: 10, y: 20 };

// Function type alias
type MathOperation = (a: number, b: number) => number;

const add: MathOperation = (a, b) => a + b;
const subtract: MathOperation = (a, b) => a - b;

// Union type alias
type StringOrNumber = string | number;
type SuccessResponse = { success: true; data: any };
type ErrorResponse = { success: false; error: string };
type ApiResponse = SuccessResponse | ErrorResponse;

Practice Exercise

Create a file called types-practice.ts and complete these tasks:

  1. Create variables of each primitive type (string, number, boolean) and assign appropriate values
  2. Create a function that accepts a parameter of type string | number and returns its string representation. Use type narrowing to handle both cases.
  3. Create a type alias called Temperature that can be either Celsius or Fahrenheit:
    type Temperature = {
        value: number;
        unit: "C" | "F";
    };
    Then create a function that converts temperatures between units.
  4. Create a function that takes a parameter of type unknown and safely logs it to the console, checking its type first
  5. Create a type alias for a user status that can only be "active", "inactive", or "suspended", and write a function that takes this status and returns an appropriate message
  6. Fix the errors in this code:
    let age: number = "25";
    let status: "active" | "inactive" = "pending";
    let data: string = null;
    
    function greet(name) {
        return "Hello, " + name;
    }

Bonus Challenge: Create a type-safe calculator that only accepts numbers and returns numbers, with proper error handling for division by zero using the never type.

Common Mistakes to Avoid

  1. Using any everywhere: This defeats the purpose of TypeScript. Use unknown instead.
  2. Not enabling strictNullChecks: Always enable this in tsconfig.json.
  3. Overusing type assertions: Prefer type guards and proper typing.
  4. Ignoring type errors: If TypeScript gives you an error, there's usually a good reason.
  5. Not leveraging type inference: Let TypeScript infer types when it's obvious.

Summary

  • Primitive types: string, number, boolean are the basic building blocks
  • any: Disables type checking - avoid when possible
  • unknown: Type-safe alternative to any - requires type checking before use
  • void: Used for functions that don't return values
  • null and undefined: Represent absence - handle explicitly with strictNullChecks
  • never: For functions that never return or unreachable code
  • Type inference: TypeScript can often determine types automatically
  • Type assertions: Override type inference when you know better (use carefully)
  • Literal types: Specify exact values a variable can have
  • Type aliases: Create reusable custom type names

Next Steps: Now that you understand basic types, we'll explore how to work with collections of data using arrays and tuples in the next lesson.