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:
- Use
const
by default - Use
let
when you need to reassign a variable - 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:
- Use
const
by default for all variables that don't need reassignment - Be aware that
const
only prevents reassignment, not mutation of objects and arrays - Consider TypeScript readonly and
Object.freeze()
for true immutability - Combine
const
with TypeScript as const assertions for more precise literal types