This section builds on the type validation section. You should also know how PFAs work.
Value-Parameterized Types
In the last section we saw Age, which was a type with custom validation that was designed for reuse.
@validate(isPositive)
type Age = number;
type Person = {
name: string;
age: Age;
}Age could not be < 0. Suppose you wanted to write a more reusable type called GreaterThan so you could write this:
type Person = {
name: string;
age: GreaterThan(0);
}You can do that using value parameterized types.
Defining a value-parameterized type
def greaterThan(minValue: number, value: number): Result<number> {
if (value > minValue) {
return success(value);
}
return failure("expected ${value} to be > ${minValue}");
}
@validate(greaterThan.partial(minValue: minValue))
type GreaterThan(minValue: number) = numberUsing a value-parameterized type
type User = {
name: string;
age: GreaterThan(0)
}Value-parameterized types vs generic types
Both kinds of types take arguments.
- Generic types take types as arguments.
- Value-parameterized types take values as arguments.
Both are used at different times.
- Generic types are used for type checking at compile time.
- Value-parameterized types are used for validation at runtime.
(Generic types are also used for validation + schema generation at runtime, but we're talking about custom validation here.)
Generic type example:
type Container<T> = {
value: T
}
const c: Container<string> = { value: "hello" }Value-parameterized type example:
def strLength(min: number, max: number, value: string): Result<string> {
if (value.length >= min && value.length <= max) {
return success(value);
}
return failure("expected ${value} to have length between ${min} and ${max}");
}
@validate(strLength.partial(min: min, max: max))
type StringOfLength(min: number, max: number) = string
const s: StringOfLength(3, 5) = "hello"Both together:
def arrayLength(length: number, value: any[]): Result<any[]> {
if (value.length === length) {
return success(value);
}
return failure("expected ${value} to have length ${length}");
}
@validate(arrayLength.partial(length: length))
type ArrayWithLength<T>(length: number) = T[]
const arr: ArrayWithLength<string>(3) = ["a", "b", "c"]Note that the value parameters are just used for validation, and validation doesn't run unless you use the schema function or the bang (!) syntax.
Syntax
Value parameters use
(...), type parameters use<...>. Value params must come after type params:tstype BoundedList<T>(n: number) = T[] const xs: BoundedList<string>(3) = ["a", "b", "c"]Defaults are allowed:
tstype Age(low: number = 0) = number const x: Age()! = 5 // uses default 0
What you can use as a value parameter
Arguments are evaluated at compile time, so they have to be statically known.
Allowed:
- String / number / boolean /
nullliterals - Multi-line
"""..."""strings - Unit literals: time (
30s,2h), cost ($5), size (100KB). - Regex literals (
re/pattern/flags); useful when forwarding to a custom validator declared with(pat: regex) static constvariable (see global vs static variables)- Object literals and array literals built from any of the above
Not allowed:
- bare function calls (
Age(getDefault())) – though if you assign the value of the function call to a static const, you can use that const as a value parameter - ternaries, binary operators, pipes
- member access (
Age(config.min))
String interpolation is restricted to value-parameter identifiers
Inside @validate(...) and @jsonSchema(...) only value parameters can be referenced in ${...} string interpolation:
// Ok
@jsonSchema({ description: "Must be divisible by ${divisor}" })
type DivisibleBy(divisor: number) = number
// Error
static const DIVISOR = 5
@jsonSchema({ description: "Must be divisible by ${DIVISOR}" })
type DivisibleBy() = numberYou could work around this by passing in a static const as the entire argument:
static const description = "Must be divisible by 5"
@jsonSchema({ description: description })
type DivisibleBy() = numberValue-parameterized types can get erased
const a: GreaterThan(0)! = 5
const sum = a.value - 6 // sum is a number, not a GreaterThan(0)You would need to annotate sum again if you wanted to keep the validation:
const sum: GreaterThan(0)! = a.value - 6