Skip to main content

Using const in TypeScript

When working with TypeScript, effectively using the const keyword is key to writing clean, maintainable, and efficient code. const is used for variables that should not change, which helps prevent bugs and makes code easier to understand.

However, its use goes beyond simple variable declarations. In this guide, we'll explore how to use const in TypeScript, covering the basics, advanced uses, best practices, and common mistakes.

Convex, as a TypeScript-focused backend platform, makes heavy use of constants to ensure type safety throughout your application, from database schemas to React components.

Declaring Constant Variables with const

To declare a constant variable in TypeScript, use the const keyword followed by the variable name and its value. This is straightforward for primitive types like numbers and strings. For example:

const MAX_SIZE = 1024;
console.log(MAX_SIZE); // Outputs: 1024

This ensures that MAX_SIZE cannot be changed, helping to prevent unintended changes in your code.

const MAX_SIZE = 1024;
MAX_SIZE = 2048; // Error: Cannot assign to 'MAX_SIZE' because it is a constant

Constants must be initialized when declared. This won't work:

const MAX_SIZE; // Error: const declarations must be initialized
MAX_SIZE = 1024;

Using constants for values that shouldn't change throughout your code makes your intentions clear and helps prevent accidental changes. When building with Convex, you'll often need to determine a variable's type using the typeof operator to ensure your code is properly typed.

Using const with Objects and Arrays

When using const with objects and arrays, remember that const only prevents the reassignment of the variable itself, not the modification of its properties or elements. Consider these examples:

const person = { name: 'John', age: 30 };
person.name = 'Jane'; // Valid - modifying a property
console.log(person); // Outputs: { name: 'Jane', age: 30 }

const numbers = [1, 2, 3];
numbers.push(4); // Valid - modifying the array
console.log(numbers); // Outputs: [1, 2, 3, 4]

However, trying to reassign the person or numbers variables will result in an error:

person = { name: 'Bob', age: 40 }; // Error: Cannot assign to 'person' because it is a constant.
numbers = [5, 6, 7]; // Error: Cannot assign to 'numbers' because it is a constant.

This behavior often surprises developers new to TypeScript. Remember that const only guarantees the variable will always reference the same object type, not that the object or array itself won't change.

When building applications with Convex, this distinction is particularly important as you'll often work with constant references to mutable data structures like database queries.

Making Objects and Arrays Truly Immutable

If you want to make objects and arrays completely immutable, you'll need additional techniques:

For Objects

Use Object.freeze() to prevent modifying an object's properties:

const immutablePerson = Object.freeze({ name: 'John', age: 30 });
immutablePerson.name = 'Jane'; // No error in TypeScript, but the change doesn't take effect
console.log(immutablePerson); // Still outputs: { name: 'John', age: 30 }

TypeScript provides the TypeScript readonly modifier for more compile-time safety:

interface Person {
readonly name: string;
readonly age: number;
}

const person: Person = { name: 'John', age: 30 };
person.name = 'Jane'; // Error: Cannot assign to 'name' because it is a read-only property

For Arrays

Use the readonly array type or the TypeScript as const assertion:

const numbers: readonly number[] = [1, 2, 3];
numbers.push(4); // Error: Property 'push' does not exist on type 'readonly number[]'

const fixedNumbers = [1, 2, 3] as const;
fixedNumbers[0] = 5; // Error: Cannot assign to '0' because it is a read-only property

When you need to modify immutable data, create new copies instead using the TypeScript spread operator:

const person = Object.freeze({ name: 'John', age: 30 });
// Create a new object rather than modifying the original
const updatedPerson = { ...person, name: 'Jane' };
console.log(updatedPerson); // Outputs: { name: 'Jane', age: 30 }

This immutable approach is popular in React applications using Convex for state management, where creating new objects rather than mutating existing ones is the standard pattern for state updates. To see this in action, check out how Convex's code spelunking article creates immutable code generation patterns.

Choosing Between const, let, and var

In TypeScript, you have three options for declaring variables: const, let, and var. Each has different scoping and reassignment rules:

const

  • Cannot be reassigned after initialization
  • Block-scoped (only accessible within the block where it's defined)
  • Must be initialized when declared
  • Best for values that shouldn't change

let

  • Can be reassigned after initialization
  • Block-scoped, like const
  • Doesn't require initialization when declared
  • Best for variables whose values will change

var

  • Can be reassigned after initialization
  • Function-scoped (hoisted to the top of the containing function)
  • Doesn't require initialization when declared
  • Generally avoided in modern TypeScript code

Here's how their scoping differs:

function scopeExample() {
if (true) {
const constVar = 'block-scoped const';
let letVar = 'block-scoped let';
var varVar = 'function-scoped var';
}

// console.log(constVar); // Error: Cannot find name 'constVar'
// console.log(letVar); // Error: Cannot find name 'letVar'
console.log(varVar); // Works, outputs: 'function-scoped var'
}

The TypeScript community generally recommends:

  1. Use const by default
  2. Use let when you need to reassign a variable
  3. Avoid var in most cases

This approach minimizes unexpected behavior and makes your code's intent clearer. The Convex TypeScript best practices guide follows these same principles for writing clean, maintainable backend code. For proper linting configuration that enforces these patterns, check out the Convex ESLint setup guide.

When working with Convex's backend, you'll often use constants to define database schemas, queries, and mutations—all while maintaining TypeScript interfaces for type safety throughout your application.

Using const for Constant Values

Using const for constant values is simple. For example:

const PI = 3.14159;
const API_URL = 'https://api.example.com';
const MAX_RETRY_ATTEMPTS = 3;

By convention, constants that represent fixed values are typically written in UPPERCASE_WITH_UNDERSCORES to distinguish them from regular variables.

Using const in Enums

TypeScript enums provide a way to define a set of named constants:

enum Direction {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT'
}

const playerDirection = Direction.Up;

For even more type safety, you can use const enum which is completely removed during compilation, resulting in more optimized JavaScript:

const enum HttpStatus {
OK = 200,
NotFound = 404,
ServerError = 500
}

const status = HttpStatus.OK; // Compiles to: const status = 200;

This pattern is helpful when working with Convex's API generation, where constants are often used to define database schemas and function parameters.

const with Literal Types

You can use const with the TypeScript typeof operator to create type-level constants:

const Colors = {
Red: '#FF0000',
Green: '#00FF00',
Blue: '#0000FF'
} as const;

// TypeScript infers: { readonly Red: "#FF0000"; readonly Green: "#00FF00"; readonly Blue: "#0000FF"; }
type ColorValues = typeof Colors[keyof typeof Colors];
// Type is: "#FF0000" | "#00FF00" | "#0000FF"

This technique is helpful for creating strongly-typed string literals from object values.

Applying const in Function Parameters

Using const effectively in function parameters helps create more reliable and self-documenting code. While you can't directly mark parameters as const, you can use TypeScript's type system to achieve similar benefits:

// Use readonly modifier for array parameters
function processItems(items: readonly string[]) {
// Cannot modify the array
// items.push("new item"); // Error: Property 'push' does not exist on type 'readonly string[]'

// But you can still iterate through it
for (const item of items) {
console.log(item);
}
}

The readonly modifier prevents accidental modification of arrays passed to functions. This is especially valuable when building APIs with Convex's TypeScript functions, where preventing unexpected mutations is essential for predictable behavior.

Using Object Destructuring with const

Another effective pattern is combining object destructuring with const:

function displayUser({ name, age }: { name: string; age: number }) {
const formattedName = name.toUpperCase();
console.log(`${formattedName}, ${age} years old`);
}

This approach is frequently used when working with Convex's React hooks, where destructuring helps extract specific properties from query results and mutation functions while maintaining type safety.

Handling Default Parameters

When providing default values for parameters, const variables can help define those defaults clearly:

const DEFAULT_PAGE_SIZE = 20;
const DEFAULT_SORT_ORDER = 'ascending' as const;

function fetchData(
query: string,
pageSize = DEFAULT_PAGE_SIZE,
sortOrder = DEFAULT_SORT_ORDER
) {
// Implementation
}

This pattern makes your code more maintainable by centralizing default values. It's a common approach in Convex's data filtering helpers for database queries.

Applying const Assertions in TypeScript for Literal Types

One of TypeScript's powerful features is the as const assertion, which creates immutable literal types from your values:

// Without const assertion
const directions = ['north', 'south', 'east', 'west'];
// Type is string[]

// With const assertion
const directionsLiteral = ['north', 'south', 'east', 'west'] as const;
// Type is readonly ['north', 'south', 'east', 'west']

The as const assertion applies to all nested values, creating deeply immutable types:

const config = {
api: {
url: 'https://api.example.com',
version: 'v1',
timeoutMs: 5000
},
features: {
darkMode: true,
notifications: ['email', 'push']
}
} as const;

This makes the entire config object and all its properties readonly with exact literal types, preventing accidental modification.

Function Arguments and Return Types

When combined with TypeScript function types, const assertions enable precise type checking for function arguments:

function fetchData<T extends readonly string[]>(endpoints: T) {
// ...implementation
}

// This works
fetchData(['users', 'posts'] as const);

// This would error if you tried to modify endpoints inside the function

Enforcing Constant Values in TypeScript Interfaces

While you can't declare constant values directly in interfaces, you can use readonly properties with literal types:

interface AppSettings {
readonly API_VERSION: 'v1';
readonly MAX_USERS: 10;
readonly FEATURES: readonly ['auth', 'messaging', 'profiles'];
}

This ensures that any object implementing this interface must have these exact values. When building complex applications with Convex's type-safe database, this approach helps enforce consistent values throughout your codebase.

Troubleshooting Common Issues with const

When working with const in TypeScript, you might encounter a few common issues. Let's explore these problems and their solutions:

Error: "Cannot assign to 'variable' because it is a constant"

This error occurs when you try to reassign a constant variable:

const MAX_SIZE = 1024;
MAX_SIZE = 2048; // Error: Cannot assign to 'MAX_SIZE' because it is a constant

Solution: If you need to reassign the variable, use a different declaration like let instead. If not, modify your code to avoid reassignment.

Error: "Missing initializer in const declaration"

This error happens when you declare a const variable without initializing it:

const API_URL; // Error: const declarations must be initialized

Solution: Always initialize const variables at declaration:

const API_URL = 'https://api.example.com';

Object Mutation Despite const

Sometimes developers are surprised that object properties can still be modified even when the object is declared with const:

const user = { name: 'John', role: 'Admin' };
user.role = 'User'; // This works despite user being a const

Solution: Use Object.freeze() or TypeScript readonly properties to prevent property modification:

const user = Object.freeze({ name: 'John', role: 'Admin' });
user.role = 'User'; // No error in TypeScript, but the change won't take effect

Final Thoughts on TypeScript const

By mastering the const keyword in TypeScript, you'll create more predictable, maintainable code. From basic declarations to advanced techniques with immutable data structures, const is a powerful tool in your TypeScript toolkit.

Remember these key points:

  1. Use const by default for all variables that don't need reassignment
  2. Be aware that const only prevents reassignment, not mutation of objects and arrays
  3. Consider TypeScript readonly and Object.freeze() for true immutability
  4. Combine const with TypeScript as const assertions for more precise literal types