typesafe-ts
Version:
TypeScript utilities for type-safe error handling and optional values
131 lines (128 loc) • 5.91 kB
JavaScript
/*
Copyright (c) 2025 Allan Deutsch
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/**
* Unique symbol used to store brand identifiers on branded values.
* Guarantees no conflicts with properties from other libraries or user code.
*/
const brand_symbol = Symbol.for("typesafe-ts/brand");
/**
* Modifies a value by applying a runtime brand.
* Brands can be used to create nominal types in TypeScript so that structurally equivalent values are not assignable.
* At runtime the brand can be used to discriminate union types and works reliably even in cases where `instanceof` does not.
*
* The brand is stored using a unique symbol (`brand.symbol`), which guarantees it will not conflict
* with any properties from other tools, libraries, or your own code.
*
* @param value The value to apply a brand to.
* @param brand_string A string literal used to create a unique brand type and runtime brand.
* @returns The input value with its type modified to include the brand, and a non-enumerable property added to the runtime value.
*
* @example Use a brand to create nominal types
* ```ts
* import { brand, type Brand } from "typesafe-ts/brand";
*
* type UserId = Brand<{ id: string }, "UserId">;
* type ProductId = Brand<{ id: string }, "ProductId">;
*
* const userId: UserId = brand.apply({ id: "user-123" }, "UserId");
* const productId: ProductId = brand.apply({ id: "prod-456" }, "ProductId");
*
* function getUser(id: UserId) { ... }
*
* getUser(userId); // OK
* getUser(productId); // Type error: ProductId is not assignable to UserId
* ```
*/
function apply_brand(value, brand_string) {
const result = value;
result[brand_symbol] = brand_string;
return result;
}
/**
* Creates a branded error class factory.
* The returned class can be extended with custom error data.
*
* @param brand_string A string literal used as the error name and brand identifier
* @returns A class constructor that creates branded Error instances
* @example Create discriminated error types for validation
* ```ts
* import { brand } from "typesafe-ts/brand";
*
* class TooShortError extends brand.Error("PasswordTooShort")<{ minLength: number }> {}
* class NoNumberError extends brand.Error("PasswordNoNumber") {}
*
* function validatePassword(password: string) {
* if (password.length < 8) return new TooShortError({ minLength: 8 });
* if (!/\d/.test(password)) return new NoNumberError();
* return null;
* }
*
* const validationError = validatePassword("short");
* if (!validationError) return;
*
* const errorBrand = validationError[brand.symbol];
* switch (errorBrand) {
* case "PasswordTooShort":
* // validationError is narrowed to TooShortError
* console.log(`Password must be at least ${validationError.minLength} characters`);
* break;
* case "PasswordNoNumber":
* // validationError is narrowed to NoNumberError
* console.log("Password must contain a number");
* break;
* }
* ```
*/
function branded_error(brand_string) {
class CustomError extends Error {
constructor(data) {
const message = data?.message ?? brand_string;
super(message);
if (data) {
Object.assign(this, data);
}
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
}
const BrandedError = (brand) => {
class Base extends CustomError {
}
// Add the brand symbol as a non-enumerable property on the prototype. One copy for every instance.
// eslint-disable-next-line typesafe-ts/enforce-result-usage
Object.defineProperty(Base.prototype, brand_symbol, {
value: brand,
enumerable: false,
writable: false,
configurable: false,
});
Base.prototype.name = brand_string;
return Base;
};
return BrandedError(brand_string);
}
/**
* Factory namespace for creating and managing branded types.
* Provides utilities to apply runtime brands to values, create discriminated error types,
* and access the brand symbol for type narrowing in union discriminants.
*
* @property apply - Applies a runtime brand to a value, creating a nominal type
* @property symbol - A unique symbol used for brands. Guaranteed not to conflict with other properties
* @property Error - Factory for branded error classes that can be reliably discriminated at runtime.
*/
const brand = {
/** Applies a runtime brand to a value, creating a nominal type. */
apply: apply_brand,
/** Unique symbol used to store and access brand identifiers. */
symbol: brand_symbol,
/** Creates a branded error class factory for discriminated error handling. */
Error: branded_error,
};
Object.freeze(brand);
export { brand, apply_brand, brand_symbol, branded_error };
//# sourceMappingURL=brand.js.map