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:
- Perfect Encapsulation π: No way to access internal representation
- Evolution Safety π: Change internals without breaking external code
- Invariant Protection π‘οΈ: Prevent invalid state creation
- Clear API Boundaries π: Force usage through defined interfaces
- 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
- π― Design Clear Interfaces: Make the public API intuitive and complete
- π Document Invariants: Clearly state what properties the opaque type maintains
- π Provide Sufficient Operations: Include all necessary functionality in the module
- π‘οΈ Validate at Construction: Ensure invariants are established when creating values
- β¨ Use TypeScript Modules: Leverage module boundaries for true encapsulation
- π¨ Consider Performance: Opaque types have zero runtime cost
- π‘ Plan for Evolution: Design APIs that can grow without breaking changes
- π 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:
- π» Refactor complex systems to use opaque types for better encapsulation
- ποΈ Build libraries with opaque APIs that can evolve safely
- π Move on to our next tutorial: Phantom Types - Compile-Time Only Types
- π Apply opaque patterns to hide complex internal optimizations
- π Study functional programming languages for more abstraction inspiration
- π― 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! ππβ¨