TypeScript Constant Best Practices

TypeScript Constant Best Practices

Sunny Sun Lv4

Scattered “magic strings” haunt countless codebases. These hardcoded values, representing error codes, status indicators, or configuration settings, create a maintenance nightmare. Sharing them effectively is crucial. Here’s where TypeScript constants come in! By declaring a constant for this error code, you ensure its consistent usage, improve code clarity, and benefit from type safety.

This article will discuss a few common approaches to managing constants in a TypeScript project.

Understanding the Importance of Constants

Before diving into the methods, it’s crucial to grasp the significance of constants in TypeScript. They:

  • Enhance code readability: By giving meaningful names to constant values, you improve code comprehension.
  • Reduce errors: Prevent accidental modifications to values that should remain fixed.
  • Improve maintainability: Centralized constants make updates easier.
  • Enable type safety: TypeScript’s type system can leverage constants for better type checking.

Static class properties

One approach to managing constants in TypeScript involves using a static class. By declaring properties as public static readonly, we create immutable values accessible throughout the application without the need for instantiation. This method promotes code organization and maintainability.

1
2
3
4
5
export class AppSettings
{
static readonly HTTP_NOT_FOUND= '404';
static readonly HTTP_INTERNAL_ERROR= '500';
};

To use the defined constants, we just need to import the class and directly reference the class and property name as below.

1
2
3
import {AppSettings} from "./appsetting.ts";

if( status === AppSettings.HTTP_NOT_FOUND) ...

The class approach is simple and serves the purpose. But it has a couple of limitations:

  • The readonly modifier only works at compile time, it has no effect at runtime.

  • As we only need the static properties, the overhead associated with Class is unnecessary.

A better approach is to use const.

Leveraging const

The const keyword in TypeScript guarantees immutability, ensuring that a variable’s value remains unchanged throughout its lifecycle. Unlike class-based constants, const provides compile-time safety, preventing accidental modifications and enhancing code reliability.

1
2
3
4
5
export const payGrades = { 
low: "1",
average: "2",
high: "3"
} as const;

In this example, we use the TypeScript feature “as const”. “as const” is a TypeScript construct for literal values called const assertion . It does two things:

  • Apply readonly modifier to properties

  • Tell the compiler not to widen the literal types.

An additional benefit that comes with the use of const assertion is that we can derive types from the declared constants.

1
2
3
4
5
6
7
8
9
10
11
12
export const payGrades = { 
low: "1",
average: "2",
high: "3"
} as const;

type t = typeof payGrades;
type payGradeType = keyof t; // 'low' | 'average' | 'high'
type payValueType = t[keyof t]; // '1' | '2' | '3'

const hisPay: payValueType = '3'; //okay
const myPay: payValueType = '4'; // error

The derived types will be useful in applying type constraints.

1
2
calculateSalary(payGrade: string) // payGrade can be any string
calculateSalary(payGrade: payValueType) // payGarde strongly typed

The above example illustrates how to use the types derived from constants to enforce type safety.

Use multiple Constants Files

For a small app, a single global constants file will be sufficient. Below is an example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// constants.ts
export namespace Constants {
export const FOO = "foo";
export const BAR = "bar";
export const BAZ = "baz";
}

// dosomething.ts
import { Constants} from "./constants"

export function doSomething() {
console.log(Constants.FOO);
console.log(Constants.BAR);
console.log(Constants.BAZ);
}

However, for a large app, the number of required constants may grow quickly. Some of the constants may be applicable only to a particular module. One global constants file won’t be ideal for this scenario.

Thus for large apps, it is better to create multiple constants files, one for each module. For example, you might create a constant file for component constants, another file for database query constants, and so on.

Another way to organize different categories of constants is to use namespaces. Using the Typescript namespace allows you to group related constants and access them using dot notation. For example, we defined two namespaces: Report and Database, each with related constants.

1
2
3
4
5
6
7
8
9
10
11
// constants/report.ts
namespace Report {
export const DEFAULT_DATE_FORMAT = 'dd/MM/yyyy';
export const TEXT_COLOR = '#000000';
}

// constants/database.ts
namespace Database {
export const MAX_TIME_OUT_IN_SECONDS= 30;
export const HOST_NAME= '....';
}

Using namespace also helps prevent naming conflicts since constants within a namespace are scoped to that namespace and are not accessible outside.

Use typeof to generate union type

When there are multiple constants files, we may need to extract and combine a few constants from different files. The following example shows how to take two constants from two different modules, and generate a union type from them using typeof type operator.



// Tech Module Constants
export const TechStaffPayGrades = { 
  low: "T1", 
  average: "T2", 
  high: "T3"
} as const;

// Admin Module Constants
export const AdminStaffPayGrades = { 
  low: "A1", 
  average: "A2", 
  high: "A3"
} as const;

// import both constants file
type allPayGrades = typeof TechStaffPayGrades | typeof AdminStaffPayGrades;

type allPayValues =  allPayGrades[keyof allPayGrades]; //"T1" | "T2" | "T3" | "A1" | "A2" | "A3"

The benefit of this approach is that we avoid duplication. At the same time, we can apply the derived type constraints to achieve better type safety . The beauty of this method is that the type is automatically updated when a new constant is added to one of the constants files.

Best Practices

Choose the right method: Consider the number of constants, their relationships, and the desired level of organization when selecting a method.

  • Leverage TypeScript’s type system: Use const assertions or type aliases to enhance type safety.
  • Avoid magic strings: Replace hardcoded values with constants to improve readability and maintainability.
  • Consider using a configuration file: For complex settings, explore using external configuration files (e.g., JSON, YAML) and loading them into TypeScript constants.

Conclusion

By effectively managing constants through these strategies, you’ll enhance code readability, maintainability, and type safety. Consistent application of these techniques fosters a more robust and scalable TypeScript codebase.

  • Title: TypeScript Constant Best Practices
  • Author: Sunny Sun
  • Created at : 2024-08-15 00:00:00
  • Updated at : 2024-08-11 11:15:39
  • Link: http://coffeethinkcode.com/2024/08/15/typescript-constants-best-practices/
  • License: This work is licensed under CC BY-NC-SA 4.0.