Advanced number typing in TypeScript
Introduction TypeScript significantly enhances number typing beyond the basic number type. While declaring a variable with the number type is the first step in typing, TypeScript offers advanced features that allow you to precisely define not only the type but also the constraints and behaviors of numeric values. These advanced capabilities transform number typing into a powerful tool for data validation, code documentation, and error prevention. In this article, we'll explore advanced number typing techniques in TypeScript, from literal types to conditional types, including utility types and generic types. These techniques are essential tools for any frontend developer concerned with code quality and maintainability. Numeric literal types and unions Defining specific values Numeric literal types allow you to restrict a variable to a specific numeric value, rather than any number. // Literal type - restricts to a specific value type Zero = 0; type One = 1; let binaryDigit: 0 | 1 = 0; binaryDigit = 1; // OK binaryDigit = 2; // Error: Type '2' is not assignable to type '0 | 1'. This approach is particularly useful for representing binary states or specific numeric constants in your application. Unions of literals to restrict possible values Unions of numeric literal types allow you to create types that only accept a predefined set of numeric values. // Union of numeric literals type DiceValue = 1 | 2 | 3 | 4 | 5 | 6; type HttpSuccessCode = 200 | 201 | 204; type HttpErrorCode = 400 | 401 | 403 | 404 | 500; let statusCode: HttpSuccessCode | HttpErrorCode = 200; statusCode = 404; // OK statusCode = 302; // Error: Type '302' is not assignable to type 'HttpSuccessCode | HttpErrorCode'. This technique offers several advantages: Implicit documentation of acceptable values Compile-time checking Autocompletion in the IDE Unions of numeric literals are particularly useful for typing business constants, HTTP status codes, or enumerated values like days of the week or months of the year. Branded number types TypeScript doesn't natively distinguish between different categories of numbers (integers, positive numbers, etc.). However, we can use the "type branding" technique to create specialized numeric types. Integers vs floating-point numbers // Type to represent an integer type Integer = number & { __brand: "Integer" }; // Function to validate that a number is an integer function asInteger(value: number): Integer { if (!Number.isInteger(value)) { throw new Error(`Value ${value} is not an integer`); } return value as Integer; } // Usage const count = asInteger(42); // count is now typed as Integer Positive and negative numbers // Types to represent positive and negative numbers type PositiveNumber = number & { __brand: "PositiveNumber" }; type NegativeNumber = number & { __brand: "NegativeNumber" }; type NonNegativeNumber = number & { __brand: "NonNegativeNumber" }; // Validation functions function asPositive(value: number): PositiveNumber { if (value = B type Subtract = A extends B ? 0 : [...Array] extends [...Array, ...infer Rest] ? Rest['length'] : never; // Usage examples type Diff1 = Subtract; // Type: 4 type Diff2 = Subtract; // Type: 5 These implementations have important limitations: They only work for small numbers (generally < 20) due to TypeScript's recursion limits They don't handle negative numbers or floating-point numbers They can cause stack overflow errors for larger values These types are primarily useful for specific use cases involving small constant numbers. Generating simple numeric sequences Recursive types can be used to generate simple numeric sequences. // Type to generate a sequence of numbers from 0 to N-1 // Note: Works only for small values of N (generally < 20) type Range = Acc['length'] extends N ? Acc[number] : Range; // Usage examples type ZeroToFive = Range; // Type: 0 | 1 | 2 | 3 | 4 | 5 type Digits = Range; // Type: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 This recursive type generates a union of all integers from 0 to N-1. It's particularly useful for creating types that represent array indices or positions. Practical applications in a React context Typing numeric props with constraints Advanced number typing techniques are particularly useful for defining React props with precise constraints: // Types for props with numeric constraints type ProgressBarProps = { value: Percentage; // Must be between 0 and 100 min?: number; // Minimum value (default 0) max?: number; // Maximum value (default 100) steps?: 5 | 10 | 20 | 25; // Number of steps }; // React component with strict typing function ProgressBar({ value, min = 0, max = 100, steps }: ProgressBarProps) { // Component implementation return ( {steps && ( {/* Gene

Introduction
TypeScript significantly enhances number typing beyond the basic number
type. While declaring a variable with the number
type is the first step in typing, TypeScript offers advanced features that allow you to precisely define not only the type but also the constraints and behaviors of numeric values. These advanced capabilities transform number typing into a powerful tool for data validation, code documentation, and error prevention.
In this article, we'll explore advanced number typing techniques in TypeScript, from literal types to conditional types, including utility types and generic types. These techniques are essential tools for any frontend developer concerned with code quality and maintainability.
Numeric literal types and unions
Defining specific values
Numeric literal types allow you to restrict a variable to a specific numeric value, rather than any number.
// Literal type - restricts to a specific value
type Zero = 0;
type One = 1;
let binaryDigit: 0 | 1 = 0;
binaryDigit = 1; // OK
binaryDigit = 2; // Error: Type '2' is not assignable to type '0 | 1'.
This approach is particularly useful for representing binary states or specific numeric constants in your application.
Unions of literals to restrict possible values
Unions of numeric literal types allow you to create types that only accept a predefined set of numeric values.
// Union of numeric literals
type DiceValue = 1 | 2 | 3 | 4 | 5 | 6;
type HttpSuccessCode = 200 | 201 | 204;
type HttpErrorCode = 400 | 401 | 403 | 404 | 500;
let statusCode: HttpSuccessCode | HttpErrorCode = 200;
statusCode = 404; // OK
statusCode = 302; // Error: Type '302' is not assignable to type 'HttpSuccessCode | HttpErrorCode'.
This technique offers several advantages:
- Implicit documentation of acceptable values
- Compile-time checking
- Autocompletion in the IDE
Unions of numeric literals are particularly useful for typing business constants, HTTP status codes, or enumerated values like days of the week or months of the year.
Branded number types
TypeScript doesn't natively distinguish between different categories of numbers (integers, positive numbers, etc.). However, we can use the "type branding" technique to create specialized numeric types.
Integers vs floating-point numbers
// Type to represent an integer
type Integer = number & { __brand: "Integer" };
// Function to validate that a number is an integer
function asInteger(value: number): Integer {
if (!Number.isInteger(value)) {
throw new Error(`Value ${value} is not an integer`);
}
return value as Integer;
}
// Usage
const count = asInteger(42);
// count is now typed as Integer
Positive and negative numbers
// Types to represent positive and negative numbers
type PositiveNumber = number & { __brand: "PositiveNumber" };
type NegativeNumber = number & { __brand: "NegativeNumber" };
type NonNegativeNumber = number & { __brand: "NonNegativeNumber" };
// Validation functions
function asPositive(value: number): PositiveNumber {
if (value <= 0) {
throw new Error(`Value ${value} is not a positive number`);
}
return value as PositiveNumber;
}
function asNonNegative(value: number): NonNegativeNumber {
if (value < 0) {
throw new Error(`Value ${value} is not a non-negative number`);
}
return value as NonNegativeNumber;
}
// Usage
const age = asNonNegative(25);
const profit = asPositive(1000);
Numbers in a specific range
// Type to represent a number in a range
type NumberInRange<Min extends number, Max extends number> = number & {
__brand: `NumberInRange<${Min}, ${Max}>`;
};
// Function to validate that a number is in a range
function inRange<Min extends number, Max extends number>(
value: number,
min: Min,
max: Max
): NumberInRange<Min, Max> {
if (value >= min && value <= max) {
return value as NumberInRange<Min, Max>;
}
throw new Error(`Value ${value} is not in range [${min}, ${max}]`);
}
// Specific type for a percentage (0-100)
type Percentage = NumberInRange<0, 100>;
// Function to validate a percentage
function asPercentage(value: number): Percentage {
return inRange(value, 0, 100);
}
// Usage
const score = asPercentage(85);
const temperature = inRange(22, -50, 50);
These specialized types are particularly useful for APIs that require constraints on numeric values, such as ages, quantities, prices, etc.
Simple arithmetic operations at the type level
TypeScript allows for some simple arithmetic operations at the type level, albeit with important limitations.
Adding small numbers
// Utility type for adding two small numbers
type Add<A extends number, B extends number> = [
...Array<A extends number ? A : never>,
...Array<B extends number ? B : never>
]['length'];
// Usage examples (works only for small numbers)
type Sum1 = Add<3, 4>; // Type: 7
type Sum2 = Add<0, 5>; // Type: 5
Subtracting small numbers
// Utility type for subtraction (A - B) where A >= B
type Subtract<A extends number, B extends number> =
A extends B ? 0 :
[...Array<A extends number ? A : never>] extends
[...Array<B extends number ? B : never>, ...infer Rest]
? Rest['length']
: never;
// Usage examples
type Diff1 = Subtract<7, 3>; // Type: 4
type Diff2 = Subtract<5, 0>; // Type: 5
These implementations have important limitations:
- They only work for small numbers (generally < 20) due to TypeScript's recursion limits
- They don't handle negative numbers or floating-point numbers
- They can cause stack overflow errors for larger values
These types are primarily useful for specific use cases involving small constant numbers.
Generating simple numeric sequences
Recursive types can be used to generate simple numeric sequences.
// Type to generate a sequence of numbers from 0 to N-1
// Note: Works only for small values of N (generally < 20)
type Range<N extends number, Acc extends number[] = []> =
Acc['length'] extends N
? Acc[number]
: Range<N, [...Acc, Acc['length']]>;
// Usage examples
type ZeroToFive = Range<6>; // Type: 0 | 1 | 2 | 3 | 4 | 5
type Digits = Range<10>; // Type: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
This recursive type generates a union of all integers from 0 to N-1. It's particularly useful for creating types that represent array indices or positions.
Practical applications in a React context
Typing numeric props with constraints
Advanced number typing techniques are particularly useful for defining React props with precise constraints:
// Types for props with numeric constraints
type ProgressBarProps = {
value: Percentage; // Must be between 0 and 100
min?: number; // Minimum value (default 0)
max?: number; // Maximum value (default 100)
steps?: 5 | 10 | 20 | 25; // Number of steps
};
// React component with strict typing
function ProgressBar({ value, min = 0, max = 100, steps }: ProgressBarProps) {
// Component implementation
return (
<div className="progress-bar">
<div
className="progress-bar-fill"
style={{ width: `${value}%` }}
/>
{steps && (
<div className="progress-bar-steps">
{/* Generation of steps */}
</div>
)}
</div>
);
}
// Usage
function App() {
const progress = asPercentage(75);
return (
<div>
<ProgressBar value={progress} steps={10} />
{/* Compilation error: */}
{/* */}
{/* */}
</div>
);
}
In this example, we use advanced typing to ensure that:
- The progress value is a valid percentage (between 0 and 100)
- The number of steps is one of the predefined values (5, 10, 20, or 25)
Typing numeric states in hooks
React hooks can also benefit from advanced number typing:
// Custom hook with advanced typing
function useCounter(
initialValue: number = 0,
min: number = Number.MIN_SAFE_INTEGER,
max: number = Number.MAX_SAFE_INTEGER
) {
const [count, setCount] = useState<number>(
Math.min(Math.max(initialValue, min), max)
);
const increment = useCallback(() => {
setCount(prev => prev < max ? prev + 1 : prev);
}, [max]);
const decrement = useCallback(() => {
setCount(prev => prev > min ? prev - 1 : prev);
}, [min]);
const reset = useCallback(() => {
setCount(initialValue);
}, [initialValue]);
return { count, increment, decrement, reset };
}
// Usage with specific constraints
function ItemCounter() {
const { count, increment, decrement } = useCounter(1, 1, 10);
return (
<div>
<button onClick={decrement}>-</button>
<span>{count} items</span>
<button onClick={increment}>+</button>
</div>
);
}
This custom hook ensures that the counter value always stays within the specified limits, which is particularly useful for item counters, pagination controls, or other UI elements that manipulate constrained numeric values.
Validating numeric inputs
// Hook to handle numeric inputs with validation
function useNumericInput<T extends number>(
initialValue: T,
validator: (value: number) => T
) {
const [value, setValue] = useState<T>(initialValue);
const [error, setError] = useState<string | null>(null);
const handleChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const newValue = parseFloat(event.target.value);
if (isNaN(newValue)) {
setError("Please enter a valid number");
return;
}
try {
const validatedValue = validator(newValue);
setValue(validatedValue);
setError(null);
} catch (err) {
setError((err as Error).message);
}
}, [validator]);
return { value, error, handleChange };
}
// Usage with a percentage field
function PercentageInput() {
const { value, error, handleChange } = useNumericInput(0, asPercentage);
return (
<div>
<label>
Percentage:
<input
type="number"
value={value}
onChange={handleChange}
min="0"
max="100"
/>
</label>
{error && <p className="error">{error}</p>}
</div>
);
}
This custom hook combines React state management with TypeScript type validation to create a robust numeric input component that ensures values meet the defined constraints.
Conclusion
Advanced number typing in TypeScript goes well beyond the simple number
type. Numeric literal types, unions, type branding, and conditional types offer flexibility and type safety that transform how we design our interfaces and systems.
These techniques allow creating clearer APIs, more robust design systems, and more maintainable applications. By precisely defining the constraints and behaviors of numeric values, we reduce the risk of errors, improve autocompletion and documentation, and facilitate collaboration between team members.
Advanced number typing is a fundamental aspect of TypeScript that perfectly illustrates the language's philosophy: providing powerful tools for static code validation while preserving JavaScript's flexibility and expressiveness.