Understanding Distinct TypeScript

TypeScript has emerged as a powerful superset of JavaScript, adding static typing to the dynamic JavaScript language. One of the advanced features within TypeScript is the concept of distinct types, which can significantly enhance the type safety and readability of your code. In this blog post, we’ll delve into the fundamental concepts of distinct TypeScript, explore usage methods, common practices, and best practices.

Table of Contents

  1. Fundamental Concepts of Distinct TypeScript
  2. Usage Methods
  3. Common Practices
  4. Best Practices
  5. Conclusion
  6. References

1. Fundamental Concepts of Distinct TypeScript

What are Distinct Types?

Distinct types in TypeScript allow you to create new types that are based on existing types but are treated as separate and incompatible types by the TypeScript compiler. This is useful when you want to enforce strict typing and prevent accidental mixing of values that should be kept separate.

Branded Types

One common way to create distinct types is through branded types. A branded type is a type that has an additional “brand” property that is used to distinguish it from other types.

// Define a brand type
type Brand<K, T> = K & { __brand: T };

// Create a distinct type for US dollars
type USD = Brand<number, "USD">;

// Create a distinct type for Euros
type EUR = Brand<number, "EUR">;

// Function to convert USD to EUR
function convertUSDToEUR(amount: USD): EUR {
    // Assume a conversion rate of 0.85
    return (amount * 0.85) as EUR;
}

const usdAmount: USD = 100 as USD;
const eurAmount: EUR = convertUSDToEUR(usdAmount);

// This will cause a type error because USD and EUR are distinct types
// const wrongAssignment: USD = eurAmount; 

In the above example, we create two distinct types USD and EUR based on the number type. The __brand property is used to differentiate between the two types, and the TypeScript compiler will prevent us from accidentally assigning a value of type EUR to a variable of type USD.

2. Usage Methods

Using Interfaces for Distinct Types

Another way to create distinct types is by using interfaces.

// Define a distinct type for User IDs
interface UserId {
    id: number;
    __userIdBrand: never;
}

// Define a distinct type for Product IDs
interface ProductId {
    id: number;
    __productIdBrand: never;
}

// Function to process a user ID
function processUserId(userId: UserId) {
    console.log(`Processing user ID: ${userId.id}`);
}

const userId: UserId = { id: 1 } as UserId;
const productId: ProductId = { id: 2 } as ProductId;

// This will work
processUserId(userId);

// This will cause a type error because ProductId is not compatible with UserId
// processUserId(productId); 

In this example, we use interfaces to create distinct types for user IDs and product IDs. The __userIdBrand and __productIdBrand properties of type never are used to make the types incompatible.

3. Common Practices

Using Distinct Types for Configuration Objects

Distinct types can be very useful when working with configuration objects.

// Define a distinct type for Database Config
interface DatabaseConfig {
    host: string;
    port: number;
    __databaseConfigBrand: never;
}

// Define a distinct type for API Config
interface APIConfig {
    baseUrl: string;
    apiKey: string;
    __apiConfigBrand: never;
}

// Function to initialize the database
function initDatabase(config: DatabaseConfig) {
    console.log(`Connecting to database at ${config.host}:${config.port}`);
}

// Function to initialize the API
function initAPI(config: APIConfig) {
    console.log(`Connecting to API at ${config.baseUrl}`);
}

const databaseConfig: DatabaseConfig = { host: 'localhost', port: 5432 } as DatabaseConfig;
const apiConfig: APIConfig = { baseUrl: 'https://example.com/api', apiKey: '12345' } as APIConfig;

// This will work
initDatabase(databaseConfig);
initAPI(apiConfig);

// This will cause a type error because APIConfig is not compatible with DatabaseConfig
// initDatabase(apiConfig); 

In this example, we use distinct types for database configuration and API configuration. This helps to prevent accidentally passing an API configuration object to the database initialization function.

4. Best Practices

Keep the Branding Simple

When creating distinct types, it’s best to keep the branding mechanism simple. Using a single brand property like __brand or a property of type never is usually sufficient.

Document the Distinct Types

Make sure to document the purpose of each distinct type in your code. This will help other developers understand why the types are distinct and how they should be used.

Use Type Guards for Runtime Checks

While TypeScript provides compile-time type checking, you may also need to perform runtime checks in some cases. You can use type guards to check the type of a value at runtime.

// Define a distinct type for Positive Numbers
type PositiveNumber = Brand<number, "PositiveNumber">;

// Type guard to check if a number is a positive number
function isPositiveNumber(num: number): num is PositiveNumber {
    return num > 0;
}

const num: number = 5;
if (isPositiveNumber(num)) {
    // Now TypeScript knows that num is a PositiveNumber
    const positiveNum: PositiveNumber = num;
    console.log(`The positive number is: ${positiveNum}`);
}

5. Conclusion

Distinct TypeScript types are a powerful tool that can greatly enhance the type safety and readability of your code. By creating distinct types, you can prevent accidental mixing of values that should be kept separate and make your code more robust. Whether you use branded types, interfaces, or other techniques, incorporating distinct types into your TypeScript projects can lead to more reliable and maintainable code.

6. References