Basic Types in TypeScript
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:
- Create variables of each primitive type (string, number, boolean) and assign appropriate values
- Create a function that accepts a parameter of type
string | numberand returns its string representation. Use type narrowing to handle both cases. - Create a type alias called
Temperaturethat can be either Celsius or Fahrenheit:Then create a function that converts temperatures between units.type Temperature = { value: number; unit: "C" | "F"; }; - Create a function that takes a parameter of type
unknownand safely logs it to the console, checking its type first - 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
- 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
- Using
anyeverywhere: This defeats the purpose of TypeScript. Useunknowninstead. - Not enabling
strictNullChecks: Always enable this intsconfig.json. - Overusing type assertions: Prefer type guards and proper typing.
- Ignoring type errors: If TypeScript gives you an error, there's usually a good reason.
- Not leveraging type inference: Let TypeScript infer types when it's obvious.
Summary
- Primitive types:
string,number,booleanare 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.