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.
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
.
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.
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.
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.
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.
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}`);
}
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.