Open In App

Opaque Types In TypeScript

Last Updated : 23 Sep, 2024
Comments
Improve
Suggest changes
Like Article
Like
Report

In TypeScript Opaque types concept allows for the definition of specialized types such as strings or numbers which are derived from primitive types but do not have the characteristics of the base types, the purpose of this is to prevent specific actions regarding the type in question, or to make this type abstract.

An opaque type is a type that is designed to be used in such a way that it appears to be one thing from the outside but in reality, is quite different on the inside, this is how it is achieved that a certain type is applied about those of other types and does not permit substitution of other types which consist of parts that looks similar.

These are the following topics that we are going to discuss:

Why Use Opaque Types?

Opaque types can help in:

  • Type safety: Avoid mix-ups between different types of the same base type.
  • Encapsulation: Hiding the internal representation of a type to prevent misuse.
  • Clear code semantics: It helps us make code easier to understand by giving specific names to types.

Example 1: Creating a UserId Opaque Type, As we will see in the following example, we introduce a new type UserId which resembles a string but can only be constructed via the createUserId function, this does show that any normal string cannot be wrongly assigned to a UserId.

JavaScript
// Defining a brand type for UserId
type UserId = string & { readonly brand: unique symbol };

// Function to create a UserId from a string
function createUserId(id: string): UserId {
    return id as UserId;
}

// Using the UserId opaque type
const userId: UserId = createUserId("1234");

console.log("Created UserId:", userId);
// Output: Created UserId: 1234

// Demonstrating type safety by passing 
// userId to a function expecting a UserId
function getUserInfo(id: UserId) {
    console.log("Fetching user info for UserId:", id);
}

getUserInfo(userId);
// Output: Fetching user info for UserId: 1234

Output:

Created UserId: 1234
Fetching user info for UserId: 1234

Example 2: Enforcing Opaque Types for Product Identifiers, Another example is ProductId is a branded string construct and this cannot be confused with UserId, such approaches prevent bugs induced by passing around wrong constructs.

JavaScript
type ProductId = string & { readonly brand: unique symbol };

function createProductId(id: string): ProductId {
    return id as ProductId;
}

const productId: ProductId = createProductId("ABCD1234");

console.log("Created ProductId:", productId);
// Output: Created ProductId: ABCD1234

// Function to process a ProductId
function processProduct(id: ProductId) {
    console.log("Processing product with ProductId:", id);
}

processProduct(productId); 
// Output: Processing product with ProductId: ABCD1234

Output:

Created ProductId: ABCD1234
Processing product with ProductId: ABCD1234

Example 3: Opaque Types for Safer APIs, In this example we create a Token log it to the console and pass it to the authenticateUser function demonstrating that function will only accept the Token type not regular strings.

JavaScript
type Token = string & { readonly brand: unique symbol };

// Function to create a Token from a string
function createToken(tokenString: string): Token {
    return tokenString as Token;
}

// Function to authenticate user with a token
function authenticateUser(token: Token) {
    console.log("Authenticating user with token:", token);
}

// Creating a valid token
const userToken = createToken("secure-token-987654321");

console.log("Created Token:", userToken);
// Output: Created Token: secure-token-987654321

// Passing the token to the authenticateUser function
authenticateUser(userToken);
// Output: Authenticating user with token: secure-token-987654321

Output:

Created Token: secure-token-987654321
Authenticating user with token: secure-token-987654321

Advantages of Opaque Types

  • Type Safety: It helps us to prevents accidental use of similar base types interchangeably.
  • Readability: Code becomes more self-documenting because types provide more context about what they represent.
  • Encapsulation: You can hide implementation details from consumers of your code.
  • Error Prevention: Reduces bugs by enforcing strict typing at compile time.

Limitations of Opaque Types

  • Not Built-In: TypeScript does not have first class support for opaque types so you need to use workarounds like branded types.
  • Extra Boilerplate: You need to create explicit types and conversion functions, which may add a little boilerplate code.
  • No Runtime Guarantees: Opaque types are enforced at compile-time, but there’s no enforcement at runtime, once compiled, they are regular JavaScript types.

Conclusion

Opaque types in TypeScript are useful because they let a program define additional, more specific types on the existing ones to make the code more safe and easy to read, the opaque types also make sure that some other values which are of the same parentage (say string or number) as some base value are used in context but not interchangeable, the last approach works pretty well because it is impossible to make a mistake and replace UserId with ProductId, hence increasing the reliability of the code by decreasing the amount of bugs.


Next Article

Similar Reads