A collection of TypeScript utility functions and types with strict type safety.
yarn add @esoh/ts-utils
This package provides type-safe assertion functions and utility types that help with TypeScript development.
Extracts the type of values from an object type.
import { ValueOf } from '@esoh/ts-utils';
// With string literal types
type Colors = { red: 'red'; blue: 'blue'; green: 'green' };
type Color = ValueOf<Colors>; // 'red' | 'blue' | 'green'
// With mixed value types
type Config = {
port: number;
host: string;
debug: boolean;
};
type ConfigValue = ValueOf<Config>; // number | string | boolean
// With nested objects
type Nested = {
a: { x: number };
b: { y: string };
};
type NestedValue = ValueOf<Nested>; // { x: number } | { y: string }
Expands an object type to show all its properties in a more readable format. This is useful for both regular objects and intersection types.
import { Expand } from '@esoh/ts-utils';
// With regular objects
type User = { name: string; age: number };
type ExpandedUser = Expand<User>; // { name: string; age: number }
// With intersection types
type Complex = { a: string } & { b: number };
type Simple = Expand<Complex>; // { a: string; b: number }
Same as Expand but works recursively on nested objects and their intersection types.
import { ExpandRecursively } from '@esoh/ts-utils';
// With regular objects
type User = { name: string; age: number };
type RecursiveUser = ExpandRecursively<User>; // { name: string; age: number }
// With nested objects and intersections
type ComplexType = {
user: { name: string } & { age: number };
settings: { theme: 'light' | 'dark' } & { language: string };
};
type Expanded = ExpandRecursively<ComplexType>;
// {
// user: { name: string; age: number };
// settings: { theme: 'light' | 'dark'; language: string };
// }
Utility type that omits properties from an object type where the value extends a given type.
import { OmitPropertiesWhereValueExtendsType } from '@esoh/ts-utils';
type Config = {
name: string;
age: number;
isActive: boolean;
createdAt: Date;
updatedAt: Date;
};
// Remove all Date properties
type WithoutDates = OmitPropertiesWhereValueExtendsType<Config, Date>;
// { name: string; age: number; isActive: boolean }
// Remove all string properties
type WithoutStrings = OmitPropertiesWhereValueExtendsType<Config, string>;
// { age: number; isActive: boolean; createdAt: Date; updatedAt: Date }
// Remove all boolean properties
type WithoutBooleans = OmitPropertiesWhereValueExtendsType<Config, boolean>;
// { name: string; age: number; createdAt: Date; updatedAt: Date }
// Works with union types too
type MixedConfig = {
name: string | null;
age: number | undefined;
isActive: boolean;
createdAt: string | null;
};
// Remove properties that extend string | null
type WithoutStringOrNull = OmitPropertiesWhereValueExtendsType<MixedConfig, string | null>;
// { age: number | undefined; isActive: boolean }
Utility type that makes specified keys required in an object type.
import { RequiredKeys } from '@esoh/ts-utils';
type User = {
name: string;
email?: string;
age?: number;
};
// Make email and age required
type UserWithRequiredFields = RequiredKeys<User, 'email' | 'age'>;
// { name: string; email: string; age: number; }
// Make only email required
type UserWithRequiredEmail = RequiredKeys<User, 'email'>;
// { name: string; email: string; age?: number; }
// Works with any object type
type Config = {
port?: number;
host?: string;
debug?: boolean;
};
// Make all fields required
type RequiredConfig = RequiredKeys<Config, 'port' | 'host' | 'debug'>;
// { port: number; host: string; debug: boolean; }
import { assert, assertCondition } from '@esoh/ts-utils';
function processUser(user: unknown) {
// Using assert
assert(user && typeof user === 'object', 'User must be an object');
// Or using assertCondition (they are aliases)
assertCondition(user && typeof user === 'object', 'User must be an object');
// Using Error object
assert(user && typeof user === 'object', new Error('Invalid user object'));
// TypeScript now knows user is an object
const name = (user as { name?: string }).name;
// ...
}
import { assertDefined } from '@esoh/ts-utils';
function processName(name: string | undefined) {
// Using string message
assertDefined(name, 'Name is required');
// Using Error object
assertDefined(name, new Error('Name cannot be undefined'));
// TypeScript now knows name is a string
console.log(name.toUpperCase());
}
import { assertNotNull } from '@esoh/ts-utils';
function processValue(value: string | null) {
// Using string message
assertNotNull(value, 'Value cannot be null');
// Using Error object
assertNotNull(value, new Error('Value must not be null'));
// TypeScript now knows value is a string
console.log(value.length);
}
import { assertNotNullish } from '@esoh/ts-utils';
function processValue(value: string | null | undefined) {
// Using string message
assertNotNullish(value, 'Value must be defined and not null');
// Using Error object
assertNotNullish(value, new Error('Value must be defined and not null'));
// TypeScript now knows value is a string
console.log(value.length);
}
import { asserted } from '@esoh/ts-utils';
function processValue(value: string | null | undefined) {
// Using string message
const safeValue = asserted(value, 'Value must be defined and not null');
// TypeScript knows safeValue is a string
console.log(safeValue.length);
// Using Error object
const anotherValue = asserted(value, new Error('Value must be defined and not null'));
// TypeScript knows anotherValue is a string
console.log(anotherValue.length);
// Can be used in expressions
const length = asserted(value, 'Value required').length;
}
import { assertedExactObjKeyOf } from '@esoh/ts-utils';
const config = {
port: 3000,
host: 'localhost',
debug: true
} as const;
function getConfigValue(key: string) {
// Using string message
const validKey = assertedExactObjKeyOf(config, key, 'Invalid config key');
// TypeScript knows validKey is 'port' | 'host' | 'debug'
return config[validKey];
// Using Error object
const anotherKey = assertedExactObjKeyOf(config, key, new Error('Invalid config key'));
// TypeScript knows anotherKey is 'port' | 'host' | 'debug'
return config[anotherKey];
}
// Type-safe object access
const port = config[assertedExactObjKeyOf(config, 'port')]; // TypeScript knows this is 3000
const host = config[assertedExactObjKeyOf(config, 'host')]; // TypeScript knows this is 'localhost'
// ⚠️ IMPORTANT: assertedExactObjKeyOf should ONLY be used with exactly typed objects
// Here's an example of what NOT to do:
type MyObject = {apple: 'red'}
const myObject = {
apple: 'red',
blueberry: 'blue',
} as MyObject;
// Because myObject is not exactly typed, this won't work correctly.
// assertedExactObjKeyOf should only be used on exactly typed objects without any other properties.
const key = 'blueberry' as const;
const value = assertedExactObjKeyOf(myObject, key, 'Invalid config key');
// TypeScript will infer the type as 'never' because 'blueberry' is not in the original type
import { assertedExactObjProperty } from '@esoh/ts-utils';
const config = {
port: 3000,
host: 'localhost',
debug: true
} as const;
function getConfigValue(key: string) {
// Using string message
const value = assertedExactObjProperty(config, key, 'Invalid config key');
// TypeScript knows value is 3000 | 'localhost' | true
return value;
// Using Error object
const anotherValue = assertedExactObjProperty(config, key, new Error('Invalid config key'));
// TypeScript knows anotherValue is 3000 | 'localhost' | true
return anotherValue;
}
// Type-safe object access with property assertion
const port = assertedExactObjProperty(config, 'port'); // TypeScript knows this is 3000
const host = assertedExactObjProperty(config, 'host'); // TypeScript knows this is 'localhost'
const debug = assertedExactObjProperty(config, 'debug'); // TypeScript knows this is true
// ⚠️ IMPORTANT: assertedExactObjProperty should ONLY be used with exactly typed objects
// Here's an example of what NOT to do:
type MyObject = {apple: 'red'}
const myObject = {
apple: 'red',
blueberry: 'blue',
} as MyObject;
// Because myObject is not exactly typed, this won't work correctly.
// assertedExactObjProperty should only be used on exactly typed objects without any other properties.
const key = 'blueberry' as const;
const value = assertedExactObjProperty(myObject, key, 'Invalid config key');
// TypeScript will infer the type as 'never' because 'blueberry' is not in the original type
import { assertOneOf } from '@esoh/ts-utils';
// Define an enum array
const Status = ['active', 'inactive', 'pending'] as const;
type Status = typeof Status[number];
function processStatus(status: string) {
// Using string message
assertOneOf(status, Status, 'Invalid status value');
// TypeScript now knows status is of type Status
// ... rest of your code
// Using Error object
assertOneOf(status, Status, new Error('Invalid status value'));
// TypeScript now knows status is of type Status
// ... rest of your code
}
// Works with number enums too
const Numbers = [1, 2, 3, 4, 5] as const;
type NumberEnum = typeof Numbers[number];
function processNumber(num: number) {
assertOneOf(num, Numbers, 'Number must be between 1 and 5');
// TypeScript now knows num is of type NumberEnum
// ... rest of your code
}
// You can also use assertedOneOf to get the value back
function processStatusWithReturn(status: string) {
// Using string message
const validStatus = assertedOneOf(status, Status, 'Invalid status value');
// TypeScript knows validStatus is of type Status
return validStatus;
// Using Error object
const anotherStatus = assertedOneOf(status, Status, new Error('Invalid status value'));
// TypeScript knows anotherStatus is of type Status
return anotherStatus;
}
import { assertNever } from '@esoh/ts-utils';
type Status = 'pending' | 'success' | 'error';
function handleStatus(status: Status) {
switch (status) {
case 'pending':
return 'Loading...';
case 'success':
return 'Done!';
case 'error':
return 'Failed!';
default:
// TypeScript will error if we forget to handle a case
return assertNever(status, 'Unhandled status');
}
}
// With discriminated unions
type Result =
| { type: 'success'; data: string }
| { type: 'error'; message: string };
function handleResult(result: Result) {
switch (result.type) {
case 'success':
return result.data;
case 'error':
return result.message;
default:
// TypeScript will error if we forget to handle a case
return assertNever(result, 'Unhandled result type');
}
}
import {
assertString, assertNumber, assertBoolean, assertObject, assertArray, assertFunction,
assertedString, assertedNumber, assertedBoolean, assertedObject, assertedArray, assertedFunction
} from '@esoh/ts-utils';
function processData(data: unknown) {
// Using assertion functions (void return)
assertString(data.name, 'Name must be a string');
assertNumber(data.age, 'Age must be a number');
assertBoolean(data.isActive, 'isActive must be a boolean');
assertObject(data.settings, 'Settings must be an object');
assertArray(data.tags, 'Tags must be an array');
assertFunction(data.handler, 'Handler must be a function');
// TypeScript now knows the types are correct
console.log(data.name.toUpperCase()); // ✅ TypeScript knows this is a string
console.log(data.age.toFixed(2)); // ✅ TypeScript knows this is a number
console.log(data.isActive ? 'Yes' : 'No'); // ✅ TypeScript knows this is a boolean
console.log(Object.keys(data.settings)); // ✅ TypeScript knows this is an object
console.log(data.tags.length); // ✅ TypeScript knows this is an array
data.handler(); // ✅ TypeScript knows this is a function
}
function processDataWithReturn(data: unknown) {
// Using asserted functions (return the value)
const name = assertedString(data.name, 'Name must be a string');
const age = assertedNumber(data.age, 'Age must be a number');
const isActive = assertedBoolean(data.isActive, 'isActive must be a boolean');
const settings = assertedObject(data.settings, 'Settings must be an object');
const tags = assertedArray(data.tags, 'Tags must be an array');
const handler = assertedFunction(data.handler, 'Handler must be a function');
// Can be used in expressions
const upperName = assertedString(data.name, 'Name required').toUpperCase();
const formattedAge = assertedNumber(data.age, 'Age required').toFixed(2);
const tagCount = assertedArray(data.tags, 'Tags required').length;
return { name, age, isActive, settings, tags, handler };
}
// Works with unknown data from external sources
function validateApiResponse(response: unknown) {
assertObject(response, 'Response must be an object');
const data = response as { user?: unknown; config?: unknown };
if (data.user) {
const user = assertedObject(data.user, 'User must be an object');
const userName = assertedString(user.name, 'User name must be a string');
const userAge = assertedNumber(user.age, 'User age must be a number');
return { userName, userAge };
}
return null;
}
import { exactObjKeys } from '@esoh/ts-utils';
// With literal types and exact object shape
const config = {
port: 3000,
host: 'localhost',
debug: true
} as const;
const configKeys = exactObjKeys(config); // type is ('port' | 'host' | 'debug')[]
// With regular object and exact shape
const user = {
name: 'John',
age: 30
};
const userKeys = exactObjKeys(user); // type is ('name' | 'age')[]
// Type-safe iteration
for (const key of exactObjKeys(config)) {
// TypeScript knows key is 'port' | 'host' | 'debug'
const value = config[key]; // TypeScript knows the exact type of each value
}
// ⚠️ Note: This function assumes the object has exactly the keys specified in its type.
// If the object might have additional properties not specified in its type,
// use Object.keys() directly:
const dynamicObj = { a: 1, b: 2, c: 3 };
const extraProps = { ...dynamicObj, d: 4, e: 5 };
const allKeys = Object.keys(extraProps); // type is string[]
import { exactObjGet } from '@esoh/ts-utils';
// With literal object and literal key
const obj1 = { a: 1, b: 2 } as const;
const value1 = exactObjGet(obj1, 'a'); // type is 1
// With literal object and wide key
const obj2 = { a: 1, b: 2 } as const;
const value2 = exactObjGet(obj2, 'a' as string); // type is 1 | 2 | undefined
// With wide object
const obj3: Record<string, number> = { a: 1, b: 2 };
const value3 = exactObjGet(obj3, 'a'); // type is number | undefined
// ⚠️ Note: When using an object with literal keys (like obj1 and obj2 above),
// the function assumes the object will only contain those exact keys and no others.
// If you need to handle objects that might have additional properties,
// use a wide type like Record<string, number> instead.
The tsAssertIsEqual
function is a compile-time type assertion that verifies two types are exactly the same. It's useful for ensuring type safety in your codebase and catching type mismatches early in development.
import { tsAssertIsEqual } from '@esoh/ts-utils';
// Basic type equality check
const a = { a: 1, b: 2 };
const b = { a: 1, b: 2 };
tsAssertIsEqual<typeof a, typeof b>(); // OK
// TypeScript will error if types don't match
const c = { a: 1, b: 2, c: 3 };
tsAssertIsEqual<typeof a, typeof c>(); // Error: c has extra property c
// Works with literal types
const obj = { a: 1, b: 2 } as const;
const value = obj.a;
tsAssertIsEqual<typeof value, 1>(); // OK
// Works with union types
const maybeValue: number | undefined = undefined;
tsAssertIsEqual<typeof maybeValue, number | undefined>(); // OK
// Works with function return types
function getValue(): number {
return 42;
}
tsAssertIsEqual<ReturnType<typeof getValue>, number>(); // OK
// Works with complex types
type ComplexType = {
a: number;
b: string;
c: boolean;
};
const complex: ComplexType = { a: 1, b: '2', c: true };
tsAssertIsEqual<typeof complex, ComplexType>(); // OK
// Catches type mismatches
const wrong: { a: string } = { a: '1' };
tsAssertIsEqual<typeof wrong, ComplexType>(); // Error: types don't match
This function is particularly useful for:
- Ensuring API contracts are maintained
- Verifying type transformations
- Testing type utilities
- Catching type mismatches in refactoring
- Documenting expected types in code
Note that this is a compile-time check with no runtime overhead, as the function has no runtime behavior.
The tsAssertExhaustiveKeys
function is a type-level assertion that ensures an array of keys exactly matches all the keys of an object type. It's particularly useful when you need to maintain a list of keys and want TypeScript to enforce that any changes to the object's keys must be reflected in the array.
import { tsAssertExhaustiveKeys } from '@esoh/ts-utils';
// Example with a configuration object
const config = {
port: 3000,
host: 'localhost',
debug: true
} as const;
// Define the keys
const keys = ['port', 'host', 'debug'] as const;
// This will pass type checking because all keys are present
tsAssertExhaustiveKeys<typeof config, typeof keys>();
// If you add a new key to config but forget to update keys, TypeScript will error
const newConfig = {
port: 3000,
host: 'localhost',
debug: true,
timeout: 5000
} as const;
// This will fail type checking because 'timeout' is missing from keys
tsAssertExhaustiveKeys<typeof newConfig, typeof keys>();
// Works with literal types
const literalConfig = {
a: 1,
b: 2,
c: 3
} as const;
const literalKeys = ['a', 'b', 'c'] as const;
tsAssertExhaustiveKeys<typeof literalConfig, typeof literalKeys>(); // OK
This function is particularly useful for:
- Maintaining lists of object keys
- Ensuring configuration objects and their key lists stay in sync
- Catching missing or extra keys during refactoring
- Documenting expected keys in code
Note that this is a compile-time check with no runtime overhead, as the function has no runtime behavior.
Type-safe version of Array.prototype.includes
that narrows down the type when checking if an array includes a value.
import { arrayIncludes } from '@esoh/ts-utils';
// With const arrays
const colors = ['red', 'blue', 'green'] as const;
const color = 'red' as string;
if (arrayIncludes(colors, color)) {
// Inside this block, TypeScript knows that color is one of: 'red' | 'blue' | 'green'
console.log(color);
}
// With regular arrays
const numbers = [1, 2, 3] as const;
const num = 1 as number;
if (arrayIncludes(numbers, num)) {
// Inside this block, TypeScript knows that num is a number
console.log(num);
}
// With union types
type Status = 'pending' | 'active' | 'inactive';
const statuses: Status[] = ['pending', 'active'];
const status = 'pending';
if (arrayIncludes(statuses, status)) {
// Inside this block, TypeScript knows that status is a Status
console.log(status);
}
The function is particularly useful when you need to narrow down the type of a value based on its presence in an array. It's more type-safe than the built-in Array.prototype.includes
because it properly narrows the type of the checked value.
Type-safe JSON value types that match the JSON specification (RFC 7159).
import { JsonPrimitive, JsonValue, JsonObject, JsonArray } from '@esoh/ts-utils';
// JsonPrimitive covers the basic JSON types
const primitive: JsonPrimitive = null; // OK
const primitive2: JsonPrimitive = "hello"; // OK
const primitive3: JsonPrimitive = 42; // OK
const primitive4: JsonPrimitive = true; // OK
// JsonValue covers all valid JSON values
const value: JsonValue = { key: "value" }; // OK
const value2: JsonValue = [1, "hello", null]; // OK
const value3: JsonValue = "simple string"; // OK
// JsonObject for JSON objects
const obj: JsonObject = {
string: "value",
number: 42,
boolean: true,
null: null,
array: [1, 2, 3],
nested: { key: "value" }
}; // OK
// JsonArray for JSON arrays
const arr: JsonArray = [
"string",
42,
true,
null,
{ key: "value" },
[1, 2, 3]
]; // OK
// TypeScript will error for non-JSON values
const invalid: JsonValue = undefined; // Error: undefined is not a valid JSON value
const invalid2: JsonValue = new Date(); // Error: Date is not a valid JSON value
const invalid3: JsonValue = () => {}; // Error: Function is not a valid JSON value
These types are useful for:
- API responses that must be JSON-serializable
- Configuration files that need to be stored as JSON
- Ensuring data structures can be safely serialized
- Type-safe JSON parsing and validation
- Strict type checking
- Runtime type assertions
- TypeScript type narrowing
- Custom error messages or Error objects
- Utility types
- Zero dependencies