Range Types Compile-time
This page covers
IntegerRangeTypeandMultipleOfRangeType: compile-time utilities that produce literal unions for bounded integer ranges. All examples use the bookstore domain. See Schemas for how schemas are registered.
See also Primary inference, Utility types.
IntegerRangeType<Min, Max>
Declaration. Produces a union of integer literals from Min to Max (inclusive). Both bounds must be non-negative integer literals within the cap of 50 (the internal IntegerRangeCap). When either bound is the general number type, or when the range exceeds the cap, the utility falls back to number.
Use this when you want a literal union for a bounded integer range - for example, 1 | 2 | 3 | 4 | 5 for a star-rating field - and you are authoring the bounds directly rather than deriving them from a schema. When bounds come from a schema, InferType<T> produces the range automatically; IntegerRangeType<Min, Max> is for explicit manual usage.
Don't use this when the range is large (over ~50 entries). Type-level integer ranges are a TypeScript-specific challenge: literal union types blow up past roughly 1 000 members, causing slow type checking and IDE lag. For large ranges, use number with a runtime validator instead. The 50-entry cap is enforced to keep compilation fast.
Signature
/**
* IntegerRangeType — Signature
*
* The canonical declaration of IntegerRangeType<TMin, TMax>: produces a
* union of integer literals from `Min` to `Max` (inclusive). Both
* bounds must be non-negative integer literals within the cap of 50.
* When either bound is the general `number` type, or the range
* exceeds the cap, the utility falls back to `number`.
*/
import type { IntegerRangeType } from '../../../src/types/index.js';
// Type declaration mirrors the canonical export in src/types/Infer.ts:
//
// export type IntegerRangeType<TMin extends number, TMax extends number>
// = number extends TMin ? number
// : number extends TMax ? number
// : RangeWithinCapType<TMax> extends true
// ? BuildIntegerRangeType<TMin, TMax>
// : number;
type StarRating = IntegerRangeType<1, 5>;
// 1 | 2 | 3 | 4 | 5
const rating: StarRating = 4;
// StarRating is the literal union 1 | 2 | 3 | 4 | 5. At runtime it is
// just a number, but only values 1..5 are assignable at compile time.
console.log('star rating:', rating);
console.log('type:', typeof rating);
Examples
Example 1: Star rating range
/**
* IntegerRangeType — Star rating range
*
* Demonstrates using IntegerRangeType to produce a literal union
* for a bounded integer range, here 1–5 for star ratings.
*/
import type {
ExhaustiveType, IntegerRangeType
} from '../../../src/types/index.js';
type StarRating = IntegerRangeType<1, 5>;
// 1 | 2 | 3 | 4 | 5
function ratingLabel(rating: StarRating): string {
switch (rating) {
case 1: return 'Poor';
case 2: return 'Fair';
case 3: return 'Good';
case 4: return 'Very Good';
case 5: return 'Excellent';
default: {
const _: ExhaustiveType<typeof rating> = rating;
return _;
}
}
}
// Demonstrate the exhaustive switch across the literal union.
const ratings: StarRating[] = [
1,
2,
3,
4,
5
];
for (const rating of ratings) {
console.log(`rating ${rating} ->`, ratingLabel(rating));
}
Example 2: Deriving automatically via InferType
/**
* IntegerRangeType — Example: Deriving automatically via InferType.
*
* When a schema declares `type: 'integer'` with a small `minimum` /
* `maximum` range, InferType produces the literal union directly.
* Reach for IntegerRangeType<Min, Max> only when you need the range
* without an underlying schema.
*/
import type {
InferType, IntegerRangeType
} from '../../../src/types/index.js';
import type { RatingScoreSchema } from '../bookstore/index.js';
// RatingScoreSchema: { type: 'integer', minimum: 1, maximum: 5 }
type Rating = InferType<typeof RatingScoreSchema>;
// 1 | 2 | 3 | 4 | 5 — derived from the schema automatically.
// Use IntegerRangeType only when you need the range without a schema:
type RatingManual = IntegerRangeType<1, 5>;
// 1 | 2 | 3 | 4 | 5 — explicit form.
// Both unions are structurally compatible.
const fromSchema: Rating = 3;
const fromManual: RatingManual = fromSchema;
// Both variables hold the same value: InferType and IntegerRangeType
// produce the same literal union 1 | 2 | 3 | 4 | 5 for these bounds.
console.log('from schema (InferType):', fromSchema);
console.log('from manual (IntegerRangeType):', fromManual);
Example 3: Small page-size range for a paginated query
/**
* IntegerRangeType — Example: Small page-size range for a paginated
* query.
*
* The `pageSize` argument is compile-time bounded between 1 and 25.
* Callers cannot pass `0` or `26`; no runtime min/max guard is needed.
*/
import type { IntegerRangeType } from '../../../src/types/index.js';
type PageSize = IntegerRangeType<1, 25>;
// 1 | 2 | 3 | ... | 25
async function fetchBooks(
page: number,
pageSize: PageSize
): Promise<readonly unknown[]> {
// pageSize is compile-time bounded — no need for a runtime guard.
void page;
void pageSize;
return [];
}
const page1 = await fetchBooks(1, 20);
console.assert(Array.isArray(page1));
console.assert(page1.length === 0);
Bad examples
Anti-pattern 1: Large ranges
/**
* Anti-pattern: Large IntegerRangeType bounds.
*
* IntegerRangeType caps at 50 entries to keep TypeScript compilation
* fast. Ranges larger than that silently fall back to `number`,
* defeating the literal-union narrowing. For wide ranges, use a
* branded `number` type or runtime validation instead.
*/
import type { IntegerRangeType } from '../../../src/types/index.js';
// ⊥ Don't do this — exceeds the cap. Even attempting to instantiate
// the type can blow the TypeScript depth limiter:
//
// type ArticleId = IntegerRangeType<1, 1000>;
//
// The utility's own design treats out-of-cap maxima as `number`, but
// you should avoid asking for the type at all. Model wide ranges with
// runtime validation:
type ArticleId = number;
// ✓ Use IntegerRangeType only at small caps:
type SmallId = IntegerRangeType<1, 10>;
// 1 | 2 | 3 | ... | 10
const wide: ArticleId = 999;
const narrow: SmallId = 7;
console.assert(typeof wide === 'number');
void narrow;
Anti-pattern 2: Floating-point bounds
/**
* Anti-pattern: Floating-point bounds.
*
* IntegerRangeType expects non-negative integer literal bounds. Passing
* floats produces undefined results — the utility is integer-only.
* For floating-point ranges, model the constraint with a branded
* `number` and a runtime check.
*/
import type { IntegerRangeType } from '../../../src/types/index.js';
// ⊥ Don't do this — bounds must be non-negative integer literals.
type PriceRange = IntegerRangeType<0.5, 9.99>;
// Produces unexpected results; the exact resolution is implementation-
// defined for non-integer bounds.
// Falls back to `number` — float bounds are not integer literals.
const _priceRange: PriceRange = 0;
void _priceRange;
// Use the integer form, then narrow with multiplication/division at the
// boundary, or carry a branded number with a runtime range check.
type PriceCents = IntegerRangeType<50, 999>;
// 50 | 51 | ... | 999 — falls back to `number` past the cap, but is
// at least integer-correct.
const cents: PriceCents = 199;
console.assert(cents === 199);
Comparison
type StarRating = IntegerRangeType<1, 5>;
// 1 | 2 | 3 | 4 | 5 - compile-time literal union, capped at 50// Zod runtime-only - no equivalent type-level literal union.
import { z } from 'zod';
const starRating = z.number().int().min(1).max(5);
type StarRating = z.infer<typeof starRating>; // number - not a literal union// TypeBox has no built-in type-level integer range utility.
// Type.Integer({ minimum: 1, maximum: 5 }) infers as number via Static<T>.
import { Type } from '@sinclair/typebox';
import type { Static } from '@sinclair/typebox';
const StarRating = Type.Integer({ minimum: 1, maximum: 5 });
type StarRating = Static<typeof StarRating>; // number - not a literal union// AJV is runtime-only - no type-level integer range.
// Declare the type manually or use a branded number.
type StarRating = 1 | 2 | 3 | 4 | 5;# Python uses Annotated with Ge/Le constraints, not literal unions:
from typing import Annotated
from pydantic import Field
StarRating = Annotated[int, Field(ge=1, le=5)]
# Validated at runtime; no equivalent type-level literal union.// Limitation: feature not directly supported in Valibot. See /comparisons for the matrix.// Limitation: feature not directly supported in Yup. See /comparisons for the matrix.// Limitation: feature not directly supported in Joi. See /comparisons for the matrix.// Limitation: feature not directly supported in io-ts. See /comparisons for the matrix.// Limitation: feature not directly supported in Effect Schema. See /comparisons for the matrix.// Limitation: feature not directly supported in ArkType. See /comparisons for the matrix.// Limitation: feature not directly supported in Runtypes. See /comparisons for the matrix.Related
MultipleOfRangeType- stepped variant (every N-th integer in a range)ExhaustiveType- pair with integer ranges for exhaustive switch checks
See also
- Constraint Brands -
MinimumBrand/MaximumBrandnumeric brands - Schemas -
minimum/maximumon integer schemas (auto-derives the range)
MultipleOfRangeType<Min, Max, Step>
Declaration. Produces a union of integer literals within [Min, Max] (inclusive) that are divisible by Step. Starts at 0, increments by Step, and includes values that fall within the range. Caps at 50 iterations; returns number when any parameter is the general number type or the cap is exceeded.
Use this when you have a multipleOf constraint on a bounded integer schema and want the resulting TypeScript type to be a precise literal union rather than number. InferType<T> produces this automatically when the schema carries both minimum/maximum and multipleOf; MultipleOfRangeType<Min, Max, Step> lets you express the same constraint explicitly without a schema.
Don't use this when the range or step combination would produce more than ~50 values - the cap kicks in and the type falls back to number. For large stepped ranges, use a branded number type with runtime validation.
Signature
/**
* MultipleOfRangeType — Signature
*
* The canonical declaration of MultipleOfRangeType<TMin, TMax, TStep>:
* produces a union of integer literals within `[Min, Max]` (inclusive)
* that are divisible by `Step`. Starts at `0`, increments by `Step`,
* includes values that fall within the range. Caps at 50 iterations;
* returns `number` when any parameter is the general `number` type or
* the cap is exceeded.
*/
import type { MultipleOfRangeType } from '../../../src/types/index.js';
// Type declaration mirrors the canonical export in src/types/Infer.ts:
//
// export type MultipleOfRangeType<
// TMin extends number, TMax extends number, TStep extends number
// >
// = number extends TMin ? number
// : number extends TMax ? number
// : number extends TStep ? number
// : BuildMultipleOfRangeType<TMin, TMax, TStep>;
type EvenQuantity = MultipleOfRangeType<0, 10, 2>;
// 0 | 2 | 4 | 6 | 8 | 10
const quantity: EvenQuantity = 6;
// EvenQuantity is the literal union 0 | 2 | 4 | 6 | 8 | 10. Odd values
// and values outside the range are rejected at compile time.
console.log('even quantity:', quantity);
console.log('type:', typeof quantity);
Examples
Example 1: Even numbers in a range
/**
* MultipleOfRangeType — Even numbers in a range
*
* Demonstrates using MultipleOfRangeType to produce a literal union
* for values divisible by a step within a bounded range.
*/
import type { MultipleOfRangeType } from '../../../src/types/index.js';
type EvenQuantity = MultipleOfRangeType<0, 10, 2>;
// 0 | 2 | 4 | 6 | 8 | 10
// All valid members of the literal union: 0, 2, 4, 6, 8, 10
const validQuantities: EvenQuantity[] = [
0,
2,
4,
6,
8,
10
];
console.log('MultipleOfRangeType<0, 10, 2> members:', validQuantities.join(' | '));
console.log('sample value:', validQuantities[3], '(6 is even, in 0..10)');
Example 2: Deriving automatically via InferType
/**
* MultipleOfRangeType — Example: Deriving automatically via InferType.
*
* When a schema declares both `minimum`/`maximum` and `multipleOf`,
* InferType produces the stepped literal union directly. Use
* MultipleOfRangeType<Min, Max, Step> only when you need the same
* shape without an underlying schema.
*/
import type {
InferType, MultipleOfRangeType
} from '../../../src/types/index.js';
const _EvenQuantitySchema = {
'maximum': 10,
'minimum': 0,
'multipleOf': 2,
'type': 'integer'
} as const;
type EvenQuantity = InferType<typeof _EvenQuantitySchema>;
// 0 | 2 | 4 | 6 | 8 | 10 — derived from the schema automatically.
// Use MultipleOfRangeType explicitly only when you need it without a schema:
type EvenQuantityManual = MultipleOfRangeType<0, 10, 2>;
const fromSchema: EvenQuantity = 4;
const fromManual: EvenQuantityManual = fromSchema;
// Both hold the same value: InferType and MultipleOfRangeType produce
// the same literal union 0 | 2 | 4 | 6 | 8 | 10 for these parameters.
console.log('from schema (InferType):', fromSchema);
console.log('from manual (MultipleOfRangeType):', fromManual);
Example 3: Discount tiers in 5% increments
/**
* MultipleOfRangeType — Example: Discount tiers in 5% increments.
*
* The discount argument is compile-time bounded to multiples of 5 in
* the range 0–50. Callers cannot pass `7` or `60`; the union enforces
* tier conformance without a runtime check.
*/
import type { MultipleOfRangeType } from '../../../src/types/index.js';
// Discounts from 0% to 50% in 5% steps.
type DiscountPercent = MultipleOfRangeType<0, 50, 5>;
// 0 | 5 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50
function applyDiscount(price: number, discount: DiscountPercent): number {
return price * (1 - discount / 100);
}
const half = applyDiscount(20, 50);
const quarter = applyDiscount(20, 25);
const full = applyDiscount(20, 0);
console.assert(half === 10);
console.assert(quarter === 15);
console.assert(full === 20);
Bad examples
Anti-pattern 1: Stepped range that exceeds the cap
/**
* Anti-pattern: Stepped range that exceeds the cap.
*
* MultipleOfRangeType caps at 50 iterations. A 0–100 range with step 1
* would produce 101 values, exceeds the cap, and falls back to
* `number`. Use a runtime validator or branded number for wide
* ranges.
*/
import type { MultipleOfRangeType } from '../../../src/types/index.js';
// ⊥ Don't do this — exceeds the 50-iteration cap.
type AllPercentages = MultipleOfRangeType<0, 100, 1>;
type AssertEqualType<TLeft, TRight>
= [TLeft] extends [TRight] ? [TRight] extends [TLeft] ? true : false : false;
function assert<T extends true>(_proof?: T): void {
return;
}
// Falls back to `number` — no literal union is available.
assert<AssertEqualType<AllPercentages, number>>();
const anyNumber: AllPercentages = 73;
console.assert(typeof anyNumber === 'number');
Anti-pattern 2: Step of zero
/**
* Anti-pattern: Step of zero.
*
* MultipleOfRangeType requires a positive non-zero step. Step `0` would
* produce an infinite loop in the type recursion and result in
* undefined behaviour. Always pass a positive integer step.
*/
import type { MultipleOfRangeType } from '../../../src/types/index.js';
// ⊥ Don't do this — step of 0 is undefined behaviour for the recursion.
// type BadRange = MultipleOfRangeType<0, 10, 0>;
//
// The line above is commented out because the type-level recursion is
// not bounded for step 0. Always supply a positive step.
// ✓ Do this — positive step keeps the recursion bounded.
type EvenInRange = MultipleOfRangeType<0, 10, 2>;
// 0 | 2 | 4 | 6 | 8 | 10
const value: EvenInRange = 6;
// EvenInRange is 0 | 2 | 4 | 6 | 8 | 10. Step 2 keeps the recursion
// bounded to 6 iterations. Step 0 would be an infinite loop in the type
// system and must never be used.
console.log('even value:', value);
console.log('type:', typeof value);
Comparison
type EvenQuantity = MultipleOfRangeType<0, 10, 2>;
// 0 | 2 | 4 | 6 | 8 | 10 - compile-time literal union, capped at 50 iterations// Zod runtime-only - no type-level stepped range.
import { z } from 'zod';
const evenQuantity = z.number().int().min(0).max(10).multipleOf(2);
type EvenQuantity = z.infer<typeof evenQuantity>; // number - not a literal union// TypeBox has no built-in type-level multipleOf range utility.
// Type.Integer({ minimum: 0, maximum: 10, multipleOf: 2 }) infers as number.
import { Type } from '@sinclair/typebox';
import type { Static } from '@sinclair/typebox';
const EvenQty = Type.Integer({ minimum: 0, maximum: 10, multipleOf: 2 });
type EvenQuantity = Static<typeof EvenQty>; // number - not a literal union// AJV is runtime-only - no type-level stepped range.
type EvenQuantity = number; // declare manually; validate with multipleOf at runtime# Python uses Annotated with MultipleOf constraint - validated at runtime:
from typing import Annotated
from pydantic import Field
EvenQuantity = Annotated[int, Field(ge=0, le=10, multiple_of=2)]
# No equivalent type-level literal union.// Limitation: feature not directly supported in Valibot. See /comparisons for the matrix.// Limitation: feature not directly supported in Yup. See /comparisons for the matrix.// Limitation: feature not directly supported in Joi. See /comparisons for the matrix.// Limitation: feature not directly supported in io-ts. See /comparisons for the matrix.// Limitation: feature not directly supported in Effect Schema. See /comparisons for the matrix.// Limitation: feature not directly supported in ArkType. See /comparisons for the matrix.// Limitation: feature not directly supported in Runtypes. See /comparisons for the matrix.Related
IntegerRangeType- unstepped variant (every integer in a range)ExhaustiveType- pair with stepped ranges for exhaustive switch checks
See also
- Constraint Brands -
MultipleOfBrandnumeric brand - Schemas -
multipleOfcombined withminimum/maximum(auto-derives the stepped range)