+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 85 of 355

πŸ«₯ Opaque Types: Hidden Implementation Details

Master opaque types in TypeScript to create abstraction layers that hide implementation details while maintaining type safety and interface contracts πŸš€

πŸ’ŽAdvanced
28 min read

Prerequisites

  • Advanced understanding of TypeScript type system πŸ“
  • Experience with brand types and nominal typing ⚑
  • Knowledge of module systems and encapsulation πŸ’»

What you'll learn

  • Understand opaque types and information hiding principles 🎯
  • Implement abstraction layers with complete encapsulation πŸ—οΈ
  • Build APIs that hide complex internal representations πŸ›
  • Apply opaque types to create robust, maintainable systems ✨

🎯 Introduction

Welcome to the sophisticated world of opaque types in TypeScript! πŸŽ‰ This tutorial explores how to create types that completely hide their internal structure, providing perfect abstraction and encapsulation while maintaining type safety at compile time.

You’ll discover how opaque types differ from brand types by offering complete information hiding - external code cannot access the internal structure at all. Whether you’re building libraries that need to evolve without breaking changes πŸ”„, implementing complex data structures with invariants πŸ—οΈ, or creating APIs with multiple internal representations 🎭, opaque types provide the ultimate abstraction mechanism.

By the end of this tutorial, you’ll be creating bulletproof abstractions that hide all implementation details while exposing clean, safe interfaces! Let’s make our types truly opaque! πŸ«₯

πŸ“š Understanding Opaque Types

πŸ€” What are Opaque Types?

Opaque types are abstract data types that completely hide their internal representation from external code πŸ”’. Unlike transparent types where you can see and access the structure, opaque types act like black boxes - you can only interact with them through their defined interface.

Key characteristics:

  • ✨ Complete Information Hiding: Internal structure is invisible
  • πŸš€ Interface-Only Access: Only exposed methods/functions work
  • πŸ›‘οΈ Implementation Independence: Internal changes don’t break external code
  • πŸ”„ Type Safety: Compile-time guarantees without runtime overhead

πŸ’‘ Opaque vs Brand vs Regular Types

Here’s how different type strategies compare:

// 🏷️ Regular structural type - full access to internals
interface RegularUser {
  id: string;
  name: string;
  email: string;
}

const regularUser: RegularUser = { id: "123", name: "Alice", email: "[email protected]" };
console.log(regularUser.id); // βœ… Direct access allowed

// 🎯 Brand type - same structure, different identity
type BrandedUserId = string & { readonly __brand: 'UserId' };
const brandedId = "123" as BrandedUserId;
console.log(brandedId.length); // βœ… Can still access string methods

// πŸ«₯ Opaque type - no access to internals at all
declare const OpaqueUserIdBrand: unique symbol;
type OpaqueUserId = string & { readonly [OpaqueUserIdBrand]: never };

// ❌ Cannot access internal structure
// const opaqueId: OpaqueUserId = "123"; // Error: can't create directly
// console.log(opaqueId.length); // Error: property doesn't exist on opaque type

πŸ—οΈ Benefits of Opaque Types

Here’s why opaque types are powerful for advanced TypeScript development:

  1. Perfect Encapsulation πŸ”’: No way to access internal representation
  2. Evolution Safety πŸ”„: Change internals without breaking external code
  3. Invariant Protection πŸ›‘οΈ: Prevent invalid state creation
  4. Clear API Boundaries πŸ“‹: Force usage through defined interfaces
  5. Performance Optimization ⚑: Hide complex internal optimizations

Real-world application: A SecurePassword type that hides whether it’s hashed, encrypted, or stored as plaintext - external code just knows it’s secure! πŸ”

πŸ«₯ Implementing Opaque Types

🎯 Basic Opaque Type Pattern

// 🌟 Core opaque type infrastructure
declare const OpaqueTypeId: unique symbol;

type Opaque<T, TToken = unknown> = T & {
  readonly [OpaqueTypeId]: TToken;
};

// πŸ”’ Opaque types with hidden internals
type SecureToken = Opaque<string, 'SecureToken'>;
type EncryptedData = Opaque<string, 'EncryptedData'>;
type HashedPassword = Opaque<string, 'HashedPassword'>;

// πŸ—οΈ Module-based encapsulation
export module SecureTokenModule {
  // 🎯 Private implementation details
  const TOKEN_PREFIX = 'ST_';
  const TOKEN_LENGTH = 32;
  
  // ✨ Public interface - only way to create/use tokens
  export function generate(): SecureToken {
    const randomBytes = crypto.getRandomValues(new Uint8Array(TOKEN_LENGTH));
    const tokenValue = TOKEN_PREFIX + Array.from(randomBytes)
      .map(b => b.toString(16).padStart(2, '0'))
      .join('');
    return tokenValue as SecureToken;
  }
  
  export function validate(token: SecureToken): boolean {
    const tokenStr = token as string; // Internal cast - not available outside
    return tokenStr.startsWith(TOKEN_PREFIX) && 
           tokenStr.length === TOKEN_PREFIX.length + TOKEN_LENGTH * 2;
  }
  
  export function toString(token: SecureToken): string {
    return `[SecureToken:${(token as string).substring(0, 8)}...]`;
  }
  
  export function equals(a: SecureToken, b: SecureToken): boolean {
    return (a as string) === (b as string);
  }
}

// πŸ§ͺ External usage - completely opaque!
const token1 = SecureTokenModule.generate();
const token2 = SecureTokenModule.generate();

console.log(SecureTokenModule.toString(token1)); // βœ… Works
console.log(SecureTokenModule.validate(token1)); // βœ… Works
console.log(SecureTokenModule.equals(token1, token2)); // βœ… Works

// ❌ These are all compilation errors:
// console.log(token1.length);           // Error: Property 'length' doesn't exist
// console.log(token1 + "_suffix");      // Error: Operator '+' cannot be applied
// const fake: SecureToken = "fake";     // Error: Type 'string' not assignable
// if (token1.startsWith("ST_")) {}      // Error: Property 'startsWith' doesn't exist

🏭 Advanced Opaque Type Factory

// πŸš€ Generic opaque type factory
type OpaqueFactory<TInternal, TBrand extends string> = {
  readonly brand: TBrand;
  readonly create: (value: TInternal) => Opaque<TInternal, TBrand>;
  readonly unwrap: (opaque: Opaque<TInternal, TBrand>) => TInternal;
};

function createOpaqueType<TInternal, TBrand extends string>(
  brand: TBrand,
  validator?: (value: TInternal) => boolean
): OpaqueFactory<TInternal, TBrand> {
  return {
    brand,
    create: (value: TInternal): Opaque<TInternal, TBrand> => {
      if (validator && !validator(value)) {
        throw new Error(`Invalid ${brand}: ${value}`);
      }
      return value as Opaque<TInternal, TBrand>;
    },
    unwrap: (opaque: Opaque<TInternal, TBrand>): TInternal => {
      return opaque as TInternal;
    }
  };
}

// 🎨 Create domain-specific opaque types
const EmailAddress = createOpaqueType('EmailAddress', (email: string) =>
  /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
);

const PositiveInteger = createOpaqueType('PositiveInteger', (num: number) =>
  Number.isInteger(num) && num> 0
);

const NonEmptyArray = createOpaqueType('NonEmptyArray', (arr: unknown[]) =>
  Array.isArray(arr) && arr.length> 0
);

// πŸ§ͺ Usage with validation
try {
  const email = EmailAddress.create("[email protected]");     // βœ… Valid
  const count = PositiveInteger.create(42);                  // βœ… Valid
  const items = NonEmptyArray.create([1, 2, 3]);            // βœ… Valid
  
  // ❌ These throw runtime errors:
  // const badEmail = EmailAddress.create("invalid-email");
  // const badCount = PositiveInteger.create(-5);
  // const badArray = NonEmptyArray.create([]);
  
  console.log(`Email domain: ${EmailAddress.unwrap(email).split('@')[1]}`);
  console.log(`Count squared: ${Math.pow(PositiveInteger.unwrap(count), 2)}`);
  console.log(`First item: ${NonEmptyArray.unwrap(items)[0]}`);
} catch (error) {
  console.error('Validation failed:', error.message);
}

🎨 Real-World Opaque Type Applications

πŸ” Cryptographic System

// πŸ” Cryptographic opaque types
type PlaintextData = Opaque<string, 'PlaintextData'>;
type EncryptedData = Opaque<string, 'EncryptedData'>;
type CryptoKey = Opaque<string, 'CryptoKey'>;
type DigitalSignature = Opaque<string, 'DigitalSignature'>;

// πŸ—οΈ Cryptography module with complete abstraction
export module CryptographyModule {
  // πŸ”’ Private implementation details
  const ENCRYPTION_ALGORITHM = 'AES-256-GCM';
  const KEY_SIZE = 32;
  const IV_SIZE = 16;
  
  // ✨ Key management
  export function generateKey(): CryptoKey {
    const keyBytes = crypto.getRandomValues(new Uint8Array(KEY_SIZE));
    const keyBase64 = btoa(String.fromCharCode(...keyBytes));
    return keyBase64 as CryptoKey;
  }
  
  export function importKey(keyData: string): CryptoKey | null {
    try {
      // Validate key format and size
      const keyBytes = Uint8Array.from(atob(keyData), c => c.charCodeAt(0));
      if (keyBytes.length !== KEY_SIZE) return null;
      return keyData as CryptoKey;
    } catch {
      return null;
    }
  }
  
  // πŸ” Encryption/Decryption
  export function encrypt(data: PlaintextData, key: CryptoKey): EncryptedData {
    const plaintext = data as string;
    const keyStr = key as string;
    
    // Simulate encryption (in real implementation, use WebCrypto API)
    const iv = crypto.getRandomValues(new Uint8Array(IV_SIZE));
    const ivBase64 = btoa(String.fromCharCode(...iv));
    const encrypted = btoa(plaintext + '_encrypted_with_' + keyStr.substring(0, 8));
    
    return `${ivBase64}:${encrypted}` as EncryptedData;
  }
  
  export function decrypt(data: EncryptedData, key: CryptoKey): PlaintextData | null {
    try {
      const dataStr = data as string;
      const keyStr = key as string;
      
      const [ivBase64, encryptedBase64] = dataStr.split(':');
      if (!ivBase64 || !encryptedBase64) return null;
      
      // Simulate decryption
      const decrypted = atob(encryptedBase64);
      const expectedSuffix = '_encrypted_with_' + keyStr.substring(0, 8);
      
      if (!decrypted.endsWith(expectedSuffix)) return null;
      
      const plaintext = decrypted.replace(expectedSuffix, '');
      return plaintext as PlaintextData;
    } catch {
      return null;
    }
  }
  
  // ✍️ Digital signatures
  export function sign(data: PlaintextData, key: CryptoKey): DigitalSignature {
    const dataStr = data as string;
    const keyStr = key as string;
    
    // Simulate signing (in real implementation, use WebCrypto API)
    const signature = btoa(`${dataStr}_signed_with_${keyStr.substring(0, 8)}`);
    return signature as DigitalSignature;
  }
  
  export function verify(
    data: PlaintextData, 
    signature: DigitalSignature, 
    key: CryptoKey
  ): boolean {
    try {
      const dataStr = data as string;
      const sigStr = signature as string;
      const keyStr = key as string;
      
      const expectedSignature = btoa(`${dataStr}_signed_with_${keyStr.substring(0, 8)}`);
      return sigStr === expectedSignature;
    } catch {
      return false;
    }
  }
  
  // 🎯 Safe constructors
  export function createPlaintext(data: string): PlaintextData {
    if (typeof data !== 'string') {
      throw new Error('Plaintext data must be a string');
    }
    return data as PlaintextData;
  }
  
  // πŸ” Utility functions
  export function getEncryptedSize(data: EncryptedData): number {
    return (data as string).length;
  }
  
  export function truncateForDisplay(data: PlaintextData, maxLength: number = 50): string {
    const str = data as string;
    return str.length> maxLength ? str.substring(0, maxLength) + '...' : str;
  }
}

// πŸ§ͺ Secure usage example
const cryptoKey = CryptographyModule.generateKey();
const message = CryptographyModule.createPlaintext("Top secret information! πŸ”");

// βœ… All operations go through secure module
const encrypted = CryptographyModule.encrypt(message, cryptoKey);
const signature = CryptographyModule.sign(message, cryptoKey);

console.log(`Encrypted size: ${CryptographyModule.getEncryptedSize(encrypted)} bytes`);
console.log(`Message preview: ${CryptographyModule.truncateForDisplay(message)}`);

// βœ… Verification and decryption
const isValidSignature = CryptographyModule.verify(message, signature, cryptoKey);
const decrypted = CryptographyModule.decrypt(encrypted, cryptoKey);

if (isValidSignature && decrypted) {
  console.log("βœ… Signature valid and decryption successful!");
  console.log(`Decrypted: ${CryptographyModule.truncateForDisplay(decrypted)}`);
}

// ❌ All these would be compilation errors:
// console.log(message.length);                    // Error: no direct access
// console.log(encrypted + "_tampered");           // Error: no string operations  
// const fakeKey: CryptoKey = "fake";              // Error: can't create directly
// const badMessage: PlaintextData = "direct";    // Error: must use constructor

🏦 Financial Transaction System

// πŸ’° Financial opaque types
type Money = Opaque<number, 'Money'>;
type AccountId = Opaque<string, 'AccountId'>;
type TransactionId = Opaque<string, 'TransactionId'>;
type ExchangeRate = Opaque<number, 'ExchangeRate'>;

// πŸ’± Currency-specific money types
type USD = Opaque<Money, 'USD'>;
type EUR = Opaque<Money, 'EUR'>;
type GBP = Opaque<Money, 'GBP'>;

// 🏦 Banking module with strict controls
export module BankingModule {
  // πŸ”’ Private constants and validation
  private const MIN_AMOUNT = 0.01;
  private const MAX_AMOUNT = 1000000.00;
  private const DECIMAL_PLACES = 2;
  
  // πŸ’° Money creation and validation
  export function createMoney(amount: number): Money | null {
    if (!Number.isFinite(amount)) return null;
    if (amount < MIN_AMOUNT || amount> MAX_AMOUNT) return null;
    if (Math.round(amount * 100) !== amount * 100) return null; // Check decimal places
    return amount as Money;
  }
  
  export function createUSD(money: Money): USD {
    return money as USD;
  }
  
  export function createEUR(money: Money): EUR {
    return money as EUR;
  }
  
  export function createGBP(money: Money): GBP {
    return money as GBP;
  }
  
  // 🏦 Account management
  export function createAccountId(bankCode: string, accountNumber: string): AccountId {
    if (!/^[A-Z]{4}$/.test(bankCode)) {
      throw new Error('Bank code must be 4 uppercase letters');
    }
    if (!/^\d{8,12}$/.test(accountNumber)) {
      throw new Error('Account number must be 8-12 digits');
    }
    return `${bankCode}-${accountNumber}` as AccountId;
  }
  
  export function generateTransactionId(): TransactionId {
    const timestamp = Date.now();
    const random = Math.floor(Math.random() * 10000).toString().padStart(4, '0');
    return `TXN-${timestamp}-${random}` as TransactionId;
  }
  
  // πŸ’± Currency operations
  export function addSameCurrency<T extends USD | EUR | GBP>(a: T, b: T): T {
    const amountA = a as Money as number;
    const amountB = b as Money as number;
    const result = createMoney(amountA + amountB);
    if (!result) throw new Error('Addition resulted in invalid amount');
    return result as T;
  }
  
  export function subtractSameCurrency<T extends USD | EUR | GBP>(a: T, b: T): T {
    const amountA = a as Money as number;
    const amountB = b as Money as number;
    const result = createMoney(amountA - amountB);
    if (!result) throw new Error('Subtraction resulted in invalid amount');
    return result as T;
  }
  
  export function multiplySameCurrency<T extends USD | EUR | GBP>(amount: T, factor: number): T {
    const amountValue = amount as Money as number;
    const result = createMoney(amountValue * factor);
    if (!result) throw new Error('Multiplication resulted in invalid amount');
    return result as T;
  }
  
  // πŸ’± Exchange rate operations
  export function createExchangeRate(rate: number): ExchangeRate | null {
    if (!Number.isFinite(rate) || rate <= 0) return null;
    return rate as ExchangeRate;
  }
  
  export function convertCurrency<TFrom extends USD | EUR | GBP, TTo extends USD | EUR | GBP>(
    amount: TFrom,
    rate: ExchangeRate,
    targetCurrency: 'USD' | 'EUR' | 'GBP'
  ): TTo {
    const amountValue = amount as Money as number;
    const rateValue = rate as number;
    const convertedAmount = createMoney(amountValue * rateValue);
    
    if (!convertedAmount) {
      throw new Error('Currency conversion resulted in invalid amount');
    }
    
    switch (targetCurrency) {
      case 'USD': return createUSD(convertedAmount) as TTo;
      case 'EUR': return createEUR(convertedAmount) as TTo;
      case 'GBP': return createGBP(convertedAmount) as TTo;
      default: throw new Error('Unsupported target currency');
    }
  }
  
  // πŸ“Š Display and formatting
  export function formatMoney<T extends USD | EUR | GBP>(amount: T, currency: 'USD' | 'EUR' | 'GBP'): string {
    const value = amount as Money as number;
    const symbols = { USD: '$', EUR: '€', GBP: 'Β£' };
    return `${symbols[currency]}${value.toFixed(DECIMAL_PLACES)}`;
  }
  
  export function compareMoney<T extends USD | EUR | GBP>(a: T, b: T): -1 | 0 | 1 {
    const valueA = a as Money as number;
    const valueB = b as Money as number;
    return valueA < valueB ? -1 : valueA> valueB ? 1 : 0;
  }
  
  // πŸ” Account operations
  export function getAccountBankCode(accountId: AccountId): string {
    const accountStr = accountId as string;
    return accountStr.split('-')[0];
  }
  
  export function getAccountNumber(accountId: AccountId): string {
    const accountStr = accountId as string;
    return accountStr.split('-')[1];
  }
}

// πŸ§ͺ Banking operations example
try {
  // βœ… Create accounts and amounts
  const account1 = BankingModule.createAccountId('CITI', '123456789');
  const account2 = BankingModule.createAccountId('JPMC', '987654321');
  
  const amount1 = BankingModule.createUSD(BankingModule.createMoney(1500.50)!);
  const amount2 = BankingModule.createUSD(BankingModule.createMoney(750.25)!);
  
  // βœ… Perform calculations
  const total = BankingModule.addSameCurrency(amount1, amount2);
  const difference = BankingModule.subtractSameCurrency(amount1, amount2);
  
  console.log(`Account 1: ${BankingModule.getAccountBankCode(account1)}-${BankingModule.getAccountNumber(account1)}`);
  console.log(`Total: ${BankingModule.formatMoney(total, 'USD')}`);
  console.log(`Difference: ${BankingModule.formatMoney(difference, 'USD')}`);
  
  // βœ… Currency conversion
  const eurRate = BankingModule.createExchangeRate(0.85)!;
  const amountInEur = BankingModule.convertCurrency<USD, EUR>(amount1, eurRate, 'EUR');
  console.log(`Amount in EUR: ${BankingModule.formatMoney(amountInEur, 'EUR')}`);
  
  // ❌ All these would be compilation errors:
  // console.log(amount1 + amount2);                     // Error: no direct arithmetic
  // console.log(amount1.toFixed(2));                    // Error: no direct method access
  // const fake: USD = 100;                              // Error: can't create directly
  // const mixed = amount1 + amountInEur;                // Error: different currency types
  
} catch (error) {
  console.error('Banking operation failed:', error.message);
}

πŸ”§ Advanced Opaque Type Patterns

🎯 Opaque Collections with Invariants

// πŸ“Š Opaque collection types
type SortedArray<T> = Opaque<T[], 'SortedArray'>;
type UniqueArray<T> = Opaque<T[], 'UniqueArray'>;
type NonEmptyArray<T> = Opaque<T[], 'NonEmptyArray'>;
type BoundedArray<T> = Opaque<T[], 'BoundedArray'>;

// πŸ—οΈ Collection operations module
export module CollectionModule {
  // 🎯 Sorted array operations
  export function createSortedArray<T>(
    items: T[], 
    compareFn?: (a: T, b: T) => number
  ): SortedArray<T> {
    const sorted = [...items].sort(compareFn);
    return sorted as SortedArray<T>;
  }
  
  export function insertSorted<T>(
    array: SortedArray<T>, 
    item: T, 
    compareFn?: (a: T, b: T) => number
  ): SortedArray<T> {
    const items = array as T[];
    const newItems = [...items];
    
    // Binary search for insertion point
    let left = 0;
    let right = newItems.length;
    
    while (left < right) {
      const mid = Math.floor((left + right) / 2);
      const cmp = compareFn ? compareFn(item, newItems[mid]) : 
                             item < newItems[mid] ? -1 : item> newItems[mid] ? 1 : 0;
      
      if (cmp <= 0) {
        right = mid;
      } else {
        left = mid + 1;
      }
    }
    
    newItems.splice(left, 0, item);
    return newItems as SortedArray<T>;
  }
  
  export function searchSorted<T>(
    array: SortedArray<T>, 
    item: T, 
    compareFn?: (a: T, b: T) => number
  ): number {
    const items = array as T[];
    let left = 0;
    let right = items.length - 1;
    
    while (left <= right) {
      const mid = Math.floor((left + right) / 2);
      const cmp = compareFn ? compareFn(item, items[mid]) : 
                             item < items[mid] ? -1 : item> items[mid] ? 1 : 0;
      
      if (cmp === 0) return mid;
      if (cmp < 0) right = mid - 1;
      else left = mid + 1;
    }
    
    return -1;
  }
  
  // 🎨 Unique array operations
  export function createUniqueArray<T>(items: T[]): UniqueArray<T> {
    const unique = Array.from(new Set(items));
    return unique as UniqueArray<T>;
  }
  
  export function addUnique<T>(array: UniqueArray<T>, item: T): UniqueArray<T> {
    const items = array as T[];
    if (items.includes(item)) return array;
    return [...items, item] as UniqueArray<T>;
  }
  
  export function removeUnique<T>(array: UniqueArray<T>, item: T): UniqueArray<T> {
    const items = array as T[];
    const filtered = items.filter(x => x !== item);
    return filtered as UniqueArray<T>;
  }
  
  // πŸš€ Non-empty array operations
  export function createNonEmptyArray<T>(items: T[]): NonEmptyArray<T> | null {
    if (items.length === 0) return null;
    return items as NonEmptyArray<T>;
  }
  
  export function headNonEmpty<T>(array: NonEmptyArray<T>): T {
    const items = array as T[];
    return items[0]; // Safe because we guarantee non-empty
  }
  
  export function tailNonEmpty<T>(array: NonEmptyArray<T>): T[] {
    const items = array as T[];
    return items.slice(1);
  }
  
  export function mapNonEmpty<T, U>(
    array: NonEmptyArray<T>, 
    fn: (item: T) => U
  ): NonEmptyArray<U> {
    const items = array as T[];
    const mapped = items.map(fn);
    return mapped as NonEmptyArray<U>; // Safe because input was non-empty
  }
  
  // πŸ“ Bounded array operations
  export function createBoundedArray<T>(
    items: T[], 
    maxSize: number
  ): BoundedArray<T> | null {
    if (items.length> maxSize) return null;
    return items as BoundedArray<T>;
  }
  
  export function addBounded<T>(
    array: BoundedArray<T>, 
    item: T, 
    maxSize: number
  ): BoundedArray<T> | null {
    const items = array as T[];
    if (items.length>= maxSize) return null;
    return [...items, item] as BoundedArray<T>;
  }
  
  // πŸ” Generic operations that work with any collection
  export function lengthOf<T>(array: SortedArray<T> | UniqueArray<T> | NonEmptyArray<T> | BoundedArray<T>): number {
    return (array as T[]).length;
  }
  
  export function toRegularArray<T>(array: SortedArray<T> | UniqueArray<T> | NonEmptyArray<T> | BoundedArray<T>): T[] {
    return [...(array as T[])];
  }
  
  export function contains<T>(
    array: SortedArray<T> | UniqueArray<T> | NonEmptyArray<T> | BoundedArray<T>, 
    item: T
  ): boolean {
    return (array as T[]).includes(item);
  }
}

// πŸ§ͺ Advanced collection usage
const numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5];

// βœ… Create different collection types
const sortedNumbers = CollectionModule.createSortedArray(numbers);
const uniqueNumbers = CollectionModule.createUniqueArray(numbers);
const nonEmptyNumbers = CollectionModule.createNonEmptyArray(numbers)!;
const boundedNumbers = CollectionModule.createBoundedArray(numbers.slice(0, 5), 10)!;

// βœ… Type-safe operations
const withNewSorted = CollectionModule.insertSorted(sortedNumbers, 7);
const withNewUnique = CollectionModule.addUnique(uniqueNumbers, 8);
const firstNumber = CollectionModule.headNonEmpty(nonEmptyNumbers);
const doubledNonEmpty = CollectionModule.mapNonEmpty(nonEmptyNumbers, x => x * 2);

console.log(`Sorted: [${CollectionModule.toRegularArray(withNewSorted).join(', ')}]`);
console.log(`Unique: [${CollectionModule.toRegularArray(withNewUnique).join(', ')}]`);
console.log(`First: ${firstNumber}`);
console.log(`Length of bounded: ${CollectionModule.lengthOf(boundedNumbers)}`);

// ❌ All these would be compilation errors:
// console.log(sortedNumbers[0]);                         // Error: no direct access
// sortedNumbers.push(10);                                // Error: no direct mutation
// const combined = sortedNumbers.concat(uniqueNumbers);  // Error: different types
// const mixed: SortedArray<number> = [1, 2, 3];         // Error: can't create directly

πŸ› οΈ Best Practices for Opaque Types

  1. 🎯 Design Clear Interfaces: Make the public API intuitive and complete
  2. πŸ“ Document Invariants: Clearly state what properties the opaque type maintains
  3. πŸ”„ Provide Sufficient Operations: Include all necessary functionality in the module
  4. πŸ›‘οΈ Validate at Construction: Ensure invariants are established when creating values
  5. ✨ Use TypeScript Modules: Leverage module boundaries for true encapsulation
  6. 🎨 Consider Performance: Opaque types have zero runtime cost
  7. πŸ’‘ Plan for Evolution: Design APIs that can grow without breaking changes
  8. πŸ” Test Thoroughly: Verify that external code can’t bypass the abstraction

πŸ§ͺ Hands-On Exercise

🎯 Challenge: Build a Type-Safe Database Query Builder

Create an opaque query system that prevents SQL injection and ensures type safety:

πŸ“‹ Requirements:

  • ✨ Opaque SQL query types that prevent string manipulation
  • πŸ”„ Type-safe query building with method chaining
  • πŸ›‘οΈ Parameter binding with injection prevention
  • 🎯 Result type safety based on SELECT columns
  • πŸ” Support for WHERE, JOIN, ORDER BY clauses

πŸš€ Bonus Points:

  • Add transaction support with opaque transaction types
  • Implement query optimization hints
  • Create prepared statement types
  • Build a schema validation system

πŸ’‘ Solution Preview

πŸ” Click to see partial solution
// πŸ—„οΈ Database opaque types
type SqlQuery = Opaque<string, 'SqlQuery'>;
type TableName = Opaque<string, 'TableName'>;
type ColumnName = Opaque<string, 'ColumnName'>;
type SafeValue = Opaque<string | number | boolean | null, 'SafeValue'>;

// πŸ—οΈ Query builder module
export module QueryModule {
  export function table(name: string): TableName {
    if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
      throw new Error('Invalid table name');
    }
    return name as TableName;
  }
  
  export function column(name: string): ColumnName {
    if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
      throw new Error('Invalid column name');
    }
    return name as ColumnName;
  }
  
  export function safeValue(value: string | number | boolean | null): SafeValue {
    // Sanitize and validate the value
    return value as SafeValue;
  }
  
  export function select(columns: ColumnName[], from: TableName): SqlQuery {
    const columnList = (columns as string[]).join(', ');
    const tableName = from as string;
    return `SELECT ${columnList} FROM ${tableName}` as SqlQuery;
  }
  
  export function where(query: SqlQuery, condition: string): SqlQuery {
    // Add parameterized WHERE clause
    return `${query as string} WHERE ${condition}` as SqlQuery;
  }
  
  export function execute<T>(query: SqlQuery): Promise<T[]> {
    // Execute the query safely
    console.log(`Executing: ${query as string}`);
    return Promise.resolve([] as T[]);
  }
}

console.log("πŸŽ‰ Type-safe database query system ready!");

πŸŽ“ Key Takeaways

You’ve mastered TypeScript’s most sophisticated abstraction technique! Here’s what you now control:

  • βœ… Complete information hiding with opaque type patterns πŸ’ͺ
  • βœ… Module-based encapsulation for true abstraction boundaries πŸ›‘οΈ
  • βœ… Invariant protection through controlled interfaces 🎯
  • βœ… Advanced collection types with guaranteed properties πŸ›
  • βœ… Real-world applications in cryptography, finance, and data structures πŸš€
  • βœ… API evolution safety through implementation hiding ✨
  • βœ… Zero-cost abstractions with compile-time enforcement πŸ”„

Remember: Opaque types provide the ultimate abstraction - complete hiding with perfect type safety! 🀝

🀝 Next Steps

Congratulations! πŸŽ‰ You’ve mastered the most advanced abstraction pattern in TypeScript!

Here’s what to do next:

  1. πŸ’» Refactor complex systems to use opaque types for better encapsulation
  2. πŸ—οΈ Build libraries with opaque APIs that can evolve safely
  3. πŸ“š Move on to our next tutorial: Phantom Types - Compile-Time Only Types
  4. 🌟 Apply opaque patterns to hide complex internal optimizations
  5. πŸ” Study functional programming languages for more abstraction inspiration
  6. 🎯 Design APIs that expose only what clients need to know

Remember: Opaque types are your ultimate tool for building maintainable, evolvable systems! πŸš€


Happy opaque type mastering! πŸŽ‰πŸš€βœ¨