TypeScript Variables and Data Types
/ 6 min read
Understanding TypeScript’s Type System
TypeScript’s type system is a powerful feature that brings static typing to JavaScript. It provides several key benefits:
- Compile-time Type Checking: Catches type-related errors before your code runs
- Enhanced IDE Support: Better autocomplete, refactoring, and error detection
- Self-Documenting Code: Types serve as inline documentation
- Safer Refactoring: The compiler helps ensure type safety when making changes
The type system is both optional and gradual, meaning you can:
- Mix typed and untyped code
- Control type checking strictness with compiler options
- Let TypeScript infer types automatically when possible
Variable Declarations
1. Variable Declaration Keywords
TypeScript supports three ways to declare variables, each with its own scope and mutability rules:
// let - block-scoped, mutablelet counter: number = 0;counter = 1; // OK - value can be changed
// const - block-scoped, immutable referenceconst PI: number = 3.14159;// PI = 3.14; // Error - cannot reassign const
// var - function-scoped (not recommended)var legacy: string = "old style";Best Practices:
- Use
constby default for immutable values - Use
letwhen you need to reassign values - Avoid
varas it can lead to scope-related bugs - Consider enabling
strictNullChecksfor better type safety
2. Type Annotations
Type annotations in TypeScript provide a way to explicitly specify the type of a variable. While often optional due to type inference, they’re useful for:
- Documentation
- Ensuring specific types when inference isn’t sufficient
- Preventing accidental type changes
// Basic type annotationslet name: string = "John";let age: number = 25;let isStudent: boolean = true;let notSure: any = 4; // avoid 'any' when possible
// Union types - variable can hold multiple typeslet id: string | number = "abc123";id = 123; // Also valid
// Type aliases - create custom type namestype ID = string | number;type Point = { x: number; y: number };
let userId: ID = "user123";let coordinates: Point = { x: 10, y: 20 };
// Type inference - TypeScript can infer typeslet inferredString = "Hello"; // type: stringlet inferredNumber = 42; // type: numberCommon Pitfalls and Solutions:
- Avoiding
any
// Badlet data: any = fetchData();
// Goodinterface ApiResponse { id: number; name: string;}let data: ApiResponse = fetchData();- Null and Undefined
// With strictNullCheckslet name: string | null = null;name = "John"; // OKname = undefined; // Error
// Optional propertiesinterface User { name: string; email?: string; // Optional}Complex Data Types
1. Arrays
Arrays in TypeScript can be typed in two ways, with additional features for type safety:
// Array type annotationslet numbers: number[] = [1, 2, 3, 4, 5];let strings: Array<string> = ["a", "b", "c"]; // Generic array type
// Mixed type arrays with explicit typinglet mixed: (string | number)[] = [1, "two", 3, "four"];
// Readonly arrays - prevents mutationsconst readonlyNumbers: ReadonlyArray<number> = [1, 2, 3];// readonlyNumbers[0] = 4; // Error// readonlyNumbers.push(4); // Error
// Array with specific length (tuple)let pair: [string, number] = ["hello", 42];
// Array methods with type safetynumbers.push(6); // OK// numbers.push("7"); // Error: Argument of type 'string' not assignable
// Type inference with arrayslet inferredArray = [1, 2, 3]; // Type: number[]let mixedInferred = [1, "two"]; // Type: (string | number)[]Best Practices:
- Use
ReadonlyArrayfor arrays that shouldn’t be modified - Consider using tuples when array length and types are fixed
- Leverage type inference when the intent is clear
- Use union types for mixed-type arrays
Enums
4. Computed and Constant Members
Enum members can have computed values or be constants. Understanding the difference is crucial for optimization:
// Constant enum membersenum FileAccess { // constant members None = 0, Read = 1 << 0, // 1 Write = 1 << 1, // 2 ReadWrite = Read | Write, // 3
// computed member HighestValue = getValue()}
function getValue() { return 1000;}5. Const Enums
Const enums are completely removed during compilation and inlined at use sites, providing better performance:
const enum Directions { Up = "UP", Down = "DOWN", Left = "LEFT", Right = "RIGHT"}
// Usagelet direction = Directions.Up;// Compiles to: let direction = "UP"6. Best Practices for Enums
- Choose the Right Enum Type
// Use numeric enums for flagsenum Permissions { None = 0, Read = 1 << 0, Write = 1 << 1, Execute = 1 << 2}
// Use string enums for clear debuggingenum LogLevel { Error = "ERROR", Warn = "WARN", Info = "INFO", Debug = "DEBUG"}- Use Const Enums for Performance
// Better performance, values inlinedconst enum HttpStatus { OK = 200, NotFound = 404, Error = 500}
// Regular enum - generates more codeenum Colors { Red = "#FF0000", Green = "#00FF00", Blue = "#0000FF"}- Document Enum Usage
/** * Represents the possible states of a task * @enum {string} */enum TaskStatus { /** Task is waiting to be started */ Pending = "PENDING", /** Task is currently in progress */ InProgress = "IN_PROGRESS", /** Task has been completed */ Completed = "COMPLETED", /** Task was cancelled before completion */ Cancelled = "CANCELLED"}Type Assertions
Type assertions provide a way to tell the TypeScript compiler “trust me, I know what I’m doing.” They’re useful when you have more information about a type than TypeScript can know.
// Using angle-bracket syntaxlet someValue: any = "this is a string";let strLength: number = (<string>someValue).length;
// Using 'as' syntax (preferred, especially in JSX)let otherValue: any = "hello";let len: number = (otherValue as string).length;
// Assertions with custom typesinterface User { name: string; age: number;}
let userObj: any = { name: "John", age: 30 };let user = userObj as User;Best Practices:
- Use assertions sparingly
- Prefer type declarations over assertions
- Use the
assyntax for consistency - Consider using type guards instead when possible
Type Guards
Type guards are expressions that perform runtime checks to guarantee the type of a value in a scope. They’re essential for working with union types safely:
// typeof type guardfunction processValue(value: string | number) { if (typeof value === "string") { // TypeScript knows value is a string here return value.toUpperCase(); } // TypeScript knows value is a number here return value.toFixed(2);}
// instanceof type guardclass Animal { move() { /* ... */ }}class Dog extends Animal { bark() { /* ... */ }}
function handleAnimal(animal: Animal) { if (animal instanceof Dog) { // TypeScript knows animal is Dog here animal.bark(); }}
// Custom type guardinterface Bird { fly(): void; layEggs(): void;}
interface Fish { swim(): void; layEggs(): void;}
function isFish(pet: Fish | Bird): pet is Fish { return (pet as Fish).swim !== undefined;}Best Practices
- Use specific types instead of
any - Leverage type inference when possible
- Use union types for flexibility
- Prefer interfaces for object types
- Use const assertions for immutable values
Common Patterns
1. Optional Properties
interface Config { name: string; port?: number; // Optional property timeout?: number; // Optional property}2. Readonly Properties
interface Point { readonly x: number; readonly y: number;}
const point: Point = { x: 10, y: 20 };// point.x = 30; // Error: Cannot assign to 'x' because it is a read-only propertyConclusion
Understanding TypeScript’s variable declarations and data types is fundamental to writing type-safe code. These concepts form the foundation for more advanced TypeScript features and patterns.
Series Navigation
- Previous: Introduction to TypeScript
- Next: TypeScript Functions and Methods