n4s
Version:
typed schema validation version of enforce
289 lines (242 loc) • 8.8 kB
text/typescript
/**
* Comprehensive TypeScript type tests for n4s
* These tests verify type inference, type guards, and compile-time type safety
*/
/* eslint-disable @typescript-eslint/no-unused-vars */
import { describe, it, expect } from 'vitest';
import { enforce } from '../n4s';
// Wrap in a function so runtime won't execute; TypeScript still checks it.
function typeChecks() {
// ===== BASIC TYPE INFERENCE =====
// Type guards provide proper narrowing
const test1 = enforce(1).isNumber().greaterThan(0);
const test2 = enforce('hello').isString().startsWith('h');
const test3 = enforce([1, 2, 3]).isArray().includes(1);
const test4 = enforce(true).isBoolean();
// These should cause type errors
// Type test: - greaterThan should not be available on boolean
// @ts-expect-error - greaterThan should not be available on boolean
const test5 = enforce(true).greaterThan(5);
// Type test: - startsWith should not be available on number
// @ts-expect-error - startsWith should not be available on number
const test6 = enforce(123).startsWith('1');
// Type test: - array includes() takes single value, not available on string
// @ts-expect-error - array includes() takes single value, not available on string
const test7 = enforce('hello').includes('h');
// ===== TYPE GUARDS =====
// isString type guard - valid chains
const str1 = enforce('hello').isString().startsWith('h');
const str2 = enforce('hello').isString().endsWith('o');
const str3 = enforce('hello').isString().matches(/^h/);
const str4 = enforce('hello').isString().longerThan(3);
const str5 = enforce('hello').isString().minLength(1);
// isNumber type guard - valid chains
const num1 = enforce(42).isNumber().greaterThan(0);
const num2 = enforce(42).isNumber().lessThan(100);
const num3 = enforce(42).isNumber().isEven();
const num4 = enforce(42).isNumber().isPositive();
const num5 = enforce(42).isNumber().isBetween(0, 100);
const num6 = enforce(42).isNumber().isNotNaN();
// isBoolean type guard - valid chains
const bool1 = enforce(true).isBoolean().isTrue();
const bool2 = enforce(false).isBoolean().isFalse();
const bool3 = enforce(true).isBoolean().isTruthy();
const bool4 = enforce(false).isBoolean().isFalsy();
const bool5 = enforce(true).isBoolean().equals(true);
// isArray type guard - valid chains
const arr1 = enforce([1, 2, 3]).isArray().includes(1);
const arr2 = enforce([1, 2, 3]).isArray().minLength(1);
const arr3 = enforce([1, 2, 3]).isArray().maxLength(10);
const arr4 = enforce([1, 2, 3]).isArray().lengthEquals(3);
const arr5 = enforce([1, 2, 3]).isArray().isEmpty();
const arr6 = enforce([1, 2, 3]).isArray().longerThan(2);
// isNumeric type guard - valid chains (works with numeric strings)
const numeric1 = enforce('42').isNumeric().greaterThan(0);
const numeric2 = enforce(42).isNumeric().lessThan(100);
const numeric3 = enforce('42').isNumeric().isBetween(0, 100);
const numeric4 = enforce('42').isNumeric().isPositive();
const numeric5 = enforce('42').isNumeric().isEven();
// isNull type guard
const null1 = enforce(null).isNull();
// isUndefined type guard
const undef1 = enforce(undefined).isUndefined();
// isNullish type guard
const nullish1 = enforce(null).isNullish();
const nullish2 = enforce(undefined).isNullish();
// Type guards work with unknown
const unknownValue: unknown = 42;
const strUnknown = enforce(unknownValue).isString().startsWith('x');
const numUnknown = enforce(unknownValue).isNumber().isPositive();
const boolUnknown = enforce(unknownValue).isBoolean().isTrue();
const arrUnknown = enforce(unknownValue).isArray().includes(1);
const numericUnknown = enforce(unknownValue).isNumeric().isPositive();
const nullUnknown = enforce(unknownValue).isNull();
const undefUnknown = enforce(unknownValue).isUndefined();
const nullishUnknown = enforce(unknownValue).isNullish();
// ===== SCHEMA RULES TYPE INFERENCE =====
// shape rule - infers exact type
const shape1 = enforce({ name: 'John', age: 30 }).shape({
name: enforce.isString(),
age: enforce.isNumber(),
});
// optional rule - infers union with undefined and null
const opt1 = enforce('hello').optional(enforce.isString());
const opt2 = enforce(undefined).optional(enforce.isString());
const opt3 = enforce(42).optional(enforce.isNumber().greaterThan(0));
// isArrayOf rule - infers array type
const arrOf1 = enforce([1, 2, 3]).isArrayOf(enforce.isNumber());
const arrOf2 = enforce(['a', 'b']).isArrayOf(enforce.isString());
const arrOf3 = enforce([true, false]).isArrayOf(enforce.isBoolean());
// loose rule - allows extra properties
const loose1 = enforce({ name: 'John', age: 30, extra: 'data' }).loose({
name: enforce.isString(),
age: enforce.isNumber(),
});
// partial rule - all properties optional
const partial1 = enforce({ name: 'John' }).partial({
name: enforce.isString(),
age: enforce.isNumber(),
});
// ===== COMPOUND RULES TYPE INFERENCE =====
// allOf rule - must satisfy all rules
const allOf1 = enforce(42).allOf(
enforce.isNumber(),
enforce.isNumber().greaterThan(0),
enforce.isNumber().lessThan(100),
);
// anyOf rule - must satisfy at least one rule (union type)
const anyOf1 = enforce('42').anyOf(enforce.isString(), enforce.isNumber());
// noneOf rule - must satisfy none of the rules
const noneOf1 = enforce(42).noneOf(enforce.isString(), enforce.isBoolean());
// oneOf rule - must satisfy exactly one rule
const oneOf1 = enforce('hello').oneOf(enforce.isString(), enforce.isNumber());
// ===== COMPLEX CHAINING SCENARIOS =====
// Multiple type guards in sequence
const complex1 = enforce(42).isNumber().greaterThan(0).isNumber().isEven();
// Type guard after schema rule
const complex2 = enforce([1, 2, 3])
.isArrayOf(enforce.isNumber())
.minLength(1);
// Chaining multiple validations on unknown
const complex3 = enforce(unknownValue)
.isNumber()
.greaterThan(0)
.lessThan(100)
.isEven();
// ===== TYPE INFERENCE WITH .infer PROPERTY =====
// Primitive rules
const stringRule = enforce.isString();
// @ts-expect-error
type StringType = typeof stringRule.infer; // Should be string
const numberRule = enforce.isNumber();
// @ts-expect-error
type NumberType = typeof numberRule.infer; // Should be number
const booleanRule = enforce.isBoolean();
// @ts-expect-error
type BooleanType = typeof booleanRule.infer; // Should be boolean
const arrayRule = enforce.isArray();
// @ts-expect-error
type ArrayType = typeof arrayRule.infer; // Should be unknown[]
// Schema rules
const userRule = enforce.shape({
id: enforce.isNumber(),
name: enforce.isString(),
email: enforce.optional(enforce.isString()),
});
// @ts-expect-error
type UserType = typeof userRule.infer;
// Compound rules
const stringOrNumberRule = enforce.anyOf(
enforce.isString(),
enforce.isNumber(),
);
// @ts-expect-error
type StringOrNumber = typeof stringOrNumberRule.infer; // Should be string | number
// Array of objects
const usersRule = enforce.isArrayOf(userRule);
// @ts-expect-error
type UsersType = typeof usersRule.infer;
// ===== CUSTOM RULES TYPE SAFETY =====
// Custom rules with extend should be type-safe
// (This would require the n4s namespace declaration)
// Mark all as used to avoid warnings
void [
test1,
test2,
test3,
test4,
test5,
test6,
test7,
str1,
str2,
str3,
str4,
str5,
num1,
num2,
num3,
num4,
num5,
num6,
bool1,
bool2,
bool3,
bool4,
bool5,
arr1,
arr2,
arr3,
arr4,
arr5,
arr6,
numeric1,
numeric2,
numeric3,
numeric4,
numeric5,
null1,
undef1,
nullish1,
nullish2,
strUnknown,
numUnknown,
boolUnknown,
arrUnknown,
numericUnknown,
nullUnknown,
undefUnknown,
nullishUnknown,
shape1,
opt1,
opt2,
opt3,
arrOf1,
arrOf2,
arrOf3,
loose1,
partial1,
allOf1,
anyOf1,
noneOf1,
oneOf1,
complex1,
complex2,
complex3,
];
}
// Runtime test to verify the file is recognized by vitest
describe('TypeScript Type Tests', () => {
it('compiles without errors', () => {
expect(true).toBe(true);
});
it('ensures type guards work at runtime', () => {
// Verify that type guards actually work
expect(enforce.isNumber().test(42)).toBe(true);
expect(enforce.isString().test('hello')).toBe(true);
expect(enforce.isBoolean().test(true)).toBe(true);
expect(enforce.isArray().test([])).toBe(true);
});
});
// Mark unused function as referenced for TS noUnusedLocals
void typeChecks;