Constraint Brands: Keywords
Siblings: Structural Narrowing | Validation modes reference
json-tology surfaces JSON Schema constraint keywords as compile-time phantom brands. Two values that satisfy different constraints produce incompatible TypeScript types, preventing silent misuse at compile time.
What changes
Without brands, { type: 'string', format: 'email' } and { type: 'string', format: 'uri' } both infer as string. Any string can flow between them silently. With brands enabled (the default), each constraint keyword intersects a phantom brand onto the base type. The types become structurally incompatible.
/**
* Constraint Brands: Keywords
*
* Demonstrates jt:* keyword-driven brands on canonical RareBookSchema
* and SignedFirstEditionSchema: inverseFunctional on Customer.id,
* transitive/irreflexive on Order.placedAt, and invariants on Order
* and SignedFirstEdition.
*/
import type {
InferType, SchemaReferencesMapType
} from '../../../src/types/index.js';
import {
bookstoreEntities, CustomerSchema, SignedFirstEditionSchema
} from '../bookstore/index.js';
import type {
bookstoreSchemas, OrderSchema
} from '../bookstore/index.js';
type BookstoreRefs = SchemaReferencesMapType<typeof bookstoreSchemas>;
type AssertEqualType<TLeft, TRight>
= [TLeft] extends [TRight] ? [TRight] extends [TLeft] ? true : false : false;
function assert<T extends true>(_proof?: T): void {
return;
}
// CustomerSchema.id carries jt:inverseFunctional brand — each customer ID
// maps to exactly one customer individual (a key constraint).
// BookstoreRefs resolves $ref fields to their named datatype types.
type Customer = InferType<typeof CustomerSchema, BookstoreRefs>;
type CustomerId = Customer extends { readonly 'id': infer I } ? I : never;
// CustomerId: string & InverseFunctionalBrandType
// (compile-time phantom brand, validated at instantiation)
assert<AssertEqualType<CustomerId extends string ? true : false, true>>();
// OrderSchema.placedAt carries temporal characteristics (transitive ordering
// of event placement). The schema registers an invariant ensuring consistent
// ordering across multiple orders.
type Order = InferType<typeof OrderSchema, BookstoreRefs>;
type PlacedAt = Order extends { readonly 'placedAt': infer P } ? P : never;
// PlacedAt: string (ISO 8601)
// At registration time, the Order schema includes jt:invariant rules
// that validate temporal consistency across instances
assert<AssertEqualType<PlacedAt extends string ? true : false, true>>();
// SignedFirstEditionSchema extends RareBookSchema with an invariant:
// signedFirstEditionIsSoloAuthored — only single-author books can be
// signed first editions. This is a cross-field rule that fires alongside
// structural validation.
type SignedFirstEdition = InferType<typeof SignedFirstEditionSchema, BookstoreRefs>;
type SignedAuthors = SignedFirstEdition extends
{ readonly 'authors': infer A }
? A
: never;
// SignedAuthors: a tuple constrained to at most one element (maxCardinality restriction)
// The jt:invariant constraint on SignedFirstEdition enforces that
// authors.length === 1 at validation time.
// The restriction narrows authors to a bounded tuple — it extends readonly string[].
assert<AssertEqualType<SignedAuthors extends readonly string[] ? true : false, true>>();
// All three schemas are registered in bookstoreEntities with their
// jt:* keyword constraints active. At instantiation time, data is validated
// against these invariants and the narrowed types are enforced.
// Runtime demonstration: instantiate a Customer with a UUID id (inverseFunctional key).
const customer = bookstoreEntities.instantiate(CustomerSchema, {
'customerId': '550e8400-e29b-41d4-a716-446655440000',
'email': 'bastian@bookstore.example',
'name': 'Bastian Balthazar Bux'
});
console.log('Customer customerId (inverseFunctional brand):', customer.customerId);
console.log('Customer email (format brand):', customer.email);
// SignedFirstEdition authors are constrained to a single-element tuple
// by jt:invariant (signedFirstEditionIsSoloAuthored). The schema $id
// confirms the registered constraint is active in the registry.
console.log('SignedFirstEdition $id:', SignedFirstEditionSchema.$id);
console.log('SignedFirstEdition is registered:', bookstoreEntities.registry.has(SignedFirstEditionSchema.$id));
| Brands ON (default) | Brands OFF | |
|---|---|---|
Email resolves to | string & FormatBrandInterface<'email'> | string |
Uri resolves to | string & FormatBrandInterface<'uri'> | string |
const x: Email = '' as string | compile error | compiles |
const x: Email = '' as Uri | compile error | compiles |
const x: Email = jt.instantiate(id, data) | compiles | compiles |
The only way to obtain a branded value is through the validation API (instantiate, materialize, is, value.cast, etc.). This is intentional - it enforces that data passes runtime validation before being treated as a constrained type.
Branded keywords
String constraints Compile-time + Runtime
| Keyword | Brand | Config flag | Example |
|---|---|---|---|
format | FormatBrandInterface<F> | formatBrands | format: 'email' brands as FormatBrandInterface<'email'> |
pattern | PatternBrandInterface<P> | stringBrands | pattern: '^[A-Z]' brands as PatternBrandInterface<'^[A-Z]'> |
minLength | MinLengthBrandInterface<N> | stringBrands | minLength: 5 brands as MinLengthBrandInterface<5> |
maxLength | MaxLengthBrandInterface<N> | stringBrands | maxLength: 100 brands as MaxLengthBrandInterface<100> |
contentMediaType | ContentMediaTypeBrandInterface<T> | contentBrands | contentMediaType: 'image/png' brands as ContentMediaTypeBrandInterface<'image/png'> |
contentEncoding | ContentEncodingBrandInterface<T> | contentBrands | contentEncoding: 'base64' brands as ContentEncodingBrandInterface<'base64'> |
import type { InferType } from '../../../src/types/index.js';
import { JsonTology } from '../../../src/index.js';
const PasswordSchema = {
'$id': 'urn:brands:Password',
'maxLength': 128,
'minLength': 8,
'pattern': '^(?=.*[A-Z])(?=.*[0-9])',
'type': 'string'
} as const;
type Password = InferType<typeof PasswordSchema>;
// string & MinLengthBrandType<8> & MaxLengthBrandType<128> & PatternBrandType<'^(?=.*[A-Z])(?=.*[0-9])'>
const jt = JsonTology.create({
'baseIri': 'urn:brands:',
'enableStrictGraph': false,
'schemas': [PasswordSchema]
});
// Valid password: meets minLength 8, has uppercase and digit.
const pw: Password = jt.instantiate(PasswordSchema.$id, 'Secret42!');
console.log('Branded password value:', pw);
console.log('Branded password is string:', typeof pw === 'string');
// Validation rejects a plain string that fails the pattern.
const errors = JsonTology.validate(PasswordSchema, 'weak');
console.log('Validation errors for "weak":', errors.items.map((err) => {
return err.message;
}));
Number constraints Compile-time + Runtime
| Keyword | Brand | Config flag | Example |
|---|---|---|---|
format | FormatBrandInterface<F> | formatBrands | format: 'int32' brands as FormatBrandInterface<'int32'> |
minimum | MinimumBrandInterface<N> | numericBrands | minimum: 0 brands as MinimumBrandInterface<0> |
maximum | MaximumBrandInterface<N> | numericBrands | maximum: 100 brands as MaximumBrandInterface<100> |
exclusiveMinimum | ExclusiveMinimumBrandInterface<N> | numericBrands | exclusiveMinimum: 0 brands as ExclusiveMinimumBrandInterface<0> |
exclusiveMaximum | ExclusiveMaximumBrandInterface<N> | numericBrands | exclusiveMaximum: 100 brands as ExclusiveMaximumBrandInterface<100> |
multipleOf | MultipleOfBrandInterface<N> | numericBrands | multipleOf: 5 brands as MultipleOfBrandInterface<5> |
import type { InferType } from '../../../src/types/index.js';
import { JsonTology } from '../../../src/index.js';
const PercentSchema = {
'$id': 'urn:brands:Percent',
'maximum': 100,
'minimum': 0,
'type': 'number'
} as const;
const TemperatureSchema = {
'$id': 'urn:brands:Temperature',
'minimum': -273,
'type': 'number'
} as const;
type Percent = InferType<typeof PercentSchema>;
type Temperature = InferType<typeof TemperatureSchema>;
// Percent: number & MinimumBrandType<0> & MaximumBrandType<100>
// Temperature: number & MinimumBrandType<-273>
// Each branded value is constructed only from its own schema's brand —
// the two are incompatible (different MinimumBrand values, and Percent
// additionally carries MaximumBrand<100>), so neither is assignable to the
// other.
const jt = JsonTology.create({
'baseIri': 'urn:brands:',
'enableStrictGraph': false,
'schemas': [
PercentSchema,
TemperatureSchema
]
});
const _percent: Percent = jt.instantiate(PercentSchema.$id, 0);
const _temp: Temperature = jt.instantiate(TemperatureSchema.$id, 0);
// Demonstrate incompatibility at the type level: Percent does not extend
// Temperature and vice versa.
type PercentIsNotTemperature = Percent extends Temperature ? false : true;
type TemperatureIsNotPercent = Temperature extends Percent ? false : true;
const _check: [PercentIsNotTemperature, TemperatureIsNotPercent] = [
true,
true
];
// Both are numbers at runtime but carry incompatible brands at compile time.
console.log('Percent value:', _percent, '(brand: minimum 0, maximum 100)');
console.log('Temperature value:', _temp, '(brand: minimum -273)');
// Percent does not extend Temperature — the brands are structurally distinct.
console.log('Brand incompatibility check [true, true]:', _check);
Array constraints Compile-time + Runtime
| Keyword | Brand | Config flag | Example |
|---|---|---|---|
uniqueItems | UniqueItemsBrandInterface / UniqueArrayBrandInterface<T> | arrayBrands | uniqueItems: true brands the array |
contains | ContainsBrandInterface<T> | arrayBrands | contains: { type: 'number' } brands as ContainsBrandInterface<number> |
minItems | MinItemsBrandInterface<N> | arrayBrands | minItems: 1 brands as MinItemsBrandInterface<1> |
maxItems | MaxItemsBrandInterface<N> | arrayBrands | maxItems: 10 brands as MaxItemsBrandInterface<10> |
When contains is present without items, the array element type narrows to the contains schema type.
import type { InferType } from '../../../src/types/index.js';
import { JsonTology } from '../../../src/index.js';
const TagSetSchema = {
'$id': 'urn:brands:TagSet',
'items': { 'type': 'string' },
'type': 'array',
'uniqueItems': true
} as const;
type TagSet = InferType<typeof TagSetSchema>;
// readonly string[] & UniqueItemsBrandType
const NumberArraySchema = {
'$id': 'urn:brands:NumberArray',
'contains': { 'type': 'number' },
'type': 'array'
} as const;
type NumberArray = InferType<typeof NumberArraySchema>;
// readonly number[] & ContainsBrandType<number>
const jt = JsonTology.create({
'baseIri': 'urn:brands:',
'enableStrictGraph': false,
'schemas': [
TagSetSchema,
NumberArraySchema
]
});
const tags: TagSet = jt.instantiate(TagSetSchema.$id, [
'fiction',
'fantasy',
'classic'
]);
const nums: NumberArray = jt.instantiate(NumberArraySchema.$id, [
1,
2,
3
]);
console.log('TagSet (uniqueItems brand):', tags);
console.log('NumberArray (contains<number> brand):', nums);
// Duplicate tags are rejected at runtime by uniqueItems validation.
const dupErrors = JsonTology.validate(TagSetSchema, [
'fiction',
'fiction'
]);
console.log('Duplicate-tag errors:', dupErrors.items.map((err) => {
return err.message;
}));
Object constraints Compile-time + Runtime
| Keyword | Brand | Config flag | Example |
|---|---|---|---|
minProperties | MinPropertiesBrandInterface<N> | objectBrands | minProperties: 1 brands as MinPropertiesBrandInterface<1> |
maxProperties | MaxPropertiesBrandInterface<N> | objectBrands | maxProperties: 5 brands as MaxPropertiesBrandInterface<5> |
When additionalProperties: false and properties are declared, excess property keys are flagged as never at compile time (requires objectBrands enabled):
import type { InferType } from '../../../src/types/index.js';
const _ClosedSchema = {
'additionalProperties': false,
'properties': {
'age': { 'type': 'integer' },
'name': { 'type': 'string' }
},
'type': 'object'
} as const;
type Closed = InferType<typeof _ClosedSchema>;
// compiles — only the declared properties are present.
const ok: Closed = {
'age': 30,
'name': 'Bastian Balthazar Bux'
};
// additionalProperties: false → excess keys are rejected. `extra` is not among
// Closed's keys, so `{ extra: true, ... }` is a compile error on assignment to
// Closed. Asserted at the type level — no cast, no forced value:
type ExtraIsRejected = 'extra' extends keyof Closed ? false : true;
const extraIsRejected: ExtraIsRejected = true;
console.log('Valid closed object:', ok);
console.log('Closed type permits only:', Object.keys(ok).join(', '));
console.log('Excess key "extra" rejected at compile time:', extraIsRejected);
Nominal constraints Compile-time
| Keyword | Brand | Config flag | Example |
|---|---|---|---|
$id | SchemaIdBrandInterface<TId> | nominalBrands | $id: 'https://example.com/User' makes types nominally distinct |
$schema | DialectBrandInterface<T> | nominalBrands | $schema: 'https://json-schema.org/draft/2020-12/schema' brands the dialect |
Nominal brands make structurally identical schemas produce incompatible types when they have different $id values. Use NominalSchemaType<T> to access the branded type:
import type { NominalSchemaType } from '../../../src/types/index.js';
const _UserSchema = {
'$id': 'https://example.com/User',
'properties': { 'name': { 'type': 'string' } },
'type': 'object'
} as const;
const _EmployeeSchema = {
'$id': 'https://example.com/Employee',
'properties': { 'name': { 'type': 'string' } },
'type': 'object'
} as const;
type User = NominalSchemaType<typeof _UserSchema>;
type Employee = NominalSchemaType<typeof _EmployeeSchema>;
// Structurally identical but nominally distinct - cannot assign one to the other
// The $id brands make User and Employee incompatible at compile time.
type UserIsNotEmployee = User extends Employee ? false : true;
type EmployeeIsNotUser = Employee extends User ? false : true;
const brandCheck: [UserIsNotEmployee, EmployeeIsNotUser] = [
true,
true
];
console.log('User $id:', _UserSchema.$id);
console.log('Employee $id:', _EmployeeSchema.$id);
console.log('Nominal incompatibility (User !extends Employee, Employee !extends User):', brandCheck);
Named format brands Compile-time
25 named format-brand aliases cover the full JSON Schema 2020-12 standard format set plus json-tology built-ins. Each alias specialises FormatBrandInterface<F> to a single format string so function signatures can name the required format explicitly.
The brand-first intersection ordering (FormatBrandInterface<F> & string) keeps the named brand visible in IDE hovers instead of being hidden behind string.
import type {
EmailBrandType, UuidBrandType
} from '../../../src/types/index.js';
import { JsonTology } from '../../../src/index.js';
const EmailSchema = {
'$id': 'urn:brands:Email',
'format': 'email',
'type': 'string'
} as const;
const UuidSchema = {
'$id': 'urn:brands:Uuid',
'format': 'uuid',
'type': 'string'
} as const;
const jt = JsonTology.create({
'baseIri': 'urn:brands:',
'enableStrictGraph': false,
'schemas': [
EmailSchema,
UuidSchema
]
});
// Only values produced by the validation API carry the named format brand.
const email: EmailBrandType = jt.instantiate(EmailSchema.$id, 'bastian@bookstore.example');
const uuid: UuidBrandType = jt.instantiate(UuidSchema.$id, '550e8400-e29b-41d4-a716-446655440000');
console.log('EmailBrandType value:', email);
console.log('UuidBrandType value:', uuid);
Standard format aliases
| Brand type | Format string |
|---|---|
EmailBrandInterface | 'email' |
IdnEmailBrandInterface | 'idn-email' |
UriBrandInterface | 'uri' |
UriReferenceBrandInterface | 'uri-reference' |
UriTemplateBrandInterface | 'uri-template' |
IriBrandInterface | 'iri' |
IriReferenceBrandInterface | 'iri-reference' |
UuidBrandInterface | 'uuid' |
DateBrandInterface | 'date' |
DateTimeBrandInterface | 'date-time' |
TimeBrandInterface | 'time' |
DurationBrandInterface | 'duration' |
HostnameBrandInterface | 'hostname' |
IdnHostnameBrandInterface | 'idn-hostname' |
Ipv4BrandInterface | 'ipv4' |
Ipv6BrandInterface | 'ipv6' |
RegexBrandInterface | 'regex' |
JsonPointerBrandInterface | 'json-pointer' |
RelativeJsonPointerBrandInterface | 'relative-json-pointer' |
BinaryBrandInterface | 'binary' |
ByteBrandInterface | 'byte' |
Int32BrandInterface | 'int32' |
Int64BrandInterface | 'int64' |
FloatBrandInterface | 'float' |
DoubleBrandInterface | 'double' |
Use the generic FormatBrandInterface<F> for custom format strings not covered by the named aliases.
Composition
Brands compose naturally through JSON Schema composition keywords.
allOf Compile-time + Runtime
Intersection merges brands from all branches:
import type { InferType } from '../../../src/types/index.js';
import { JsonTology } from '../../../src/index.js';
const ValidatedEmailSchema = {
'$id': 'urn:brands:ValidatedEmail',
'allOf': [
{
'format': 'email',
'type': 'string'
},
{
'minLength': 5,
'type': 'string'
}
]
} as const;
type VEmail = InferType<typeof ValidatedEmailSchema>;
// string & FormatBrandType<'email'> & string & MinLengthBrandType<5>
// simplifies to: string & FormatBrandType<'email'> & MinLengthBrandType<5>
const jt = JsonTology.create({
'baseIri': 'urn:brands:',
'enableStrictGraph': false,
'schemas': [ValidatedEmailSchema]
});
// allOf intersects brands from both branches: email format AND minLength 5.
const vemail: VEmail = jt.instantiate(ValidatedEmailSchema.$id, 'user@example.com');
console.log('allOf branded email:', vemail);
// Validation rejects strings that fail either branch.
const shortErrors = JsonTology.validate(ValidatedEmailSchema, 'a@b');
console.log('Errors for "a@b" (too short):', shortErrors.items.map((err) => {
return err.message;
}));
anyOf / oneOf Compile-time + Runtime
Union preserves each branch's brands independently:
import type { InferType } from '../../../src/types/index.js';
import { JsonTology } from '../../../src/index.js';
const IdSchema = {
'$id': 'urn:brands:Id',
'oneOf': [
{
'format': 'uuid',
'type': 'string'
},
{
'minimum': 1,
'type': 'number'
}
]
} as const;
type Id = InferType<typeof IdSchema>;
// (string & FormatBrandType<'uuid'>) | (number & MinimumBrandType<1>)
const jt = JsonTology.create({
'baseIri': 'urn:brands:',
'enableStrictGraph': false,
'schemas': [IdSchema]
});
// oneOf preserves each branch's brand independently as a union.
const stringId: Id = jt.instantiate(IdSchema.$id, '550e8400-e29b-41d4-a716-446655440000');
const numberId: Id = jt.instantiate(IdSchema.$id, 42);
console.log('String branch (uuid brand):', stringId);
console.log('Number branch (minimum 1 brand):', numberId);
Utility types
DeprecatedKeysType<T> / NonDeprecatedSchemaType<T>
Filter deprecated properties from a schema type:
import type {
DeprecatedKeysType, NonDeprecatedSchemaType
} from '../../../src/types/index.js';
const _UserSchema = {
'properties': {
'legacyId': {
'deprecated': true,
'type': 'string'
},
'name': { 'type': 'string' }
},
'required': ['name'],
'type': 'object'
} as const;
// 'legacyId'
type DepKeys = DeprecatedKeysType<typeof _UserSchema>;
// { name: string } - no legacyId
type User = NonDeprecatedSchemaType<typeof _UserSchema>;
// DepKeys is a union of deprecated property names. User omits them.
type DepKeysIsLegacyId = DepKeys extends 'legacyId' ? true : false;
type UserHasNoLegacyId = User extends { 'legacyId': unknown } ? false : true;
const check: [DepKeysIsLegacyId, UserHasNoLegacyId] = [
true,
true
];
console.log('Deprecated key is "legacyId":', check[0]);
console.log('NonDeprecated type omits legacyId:', check[1]);
EnumValuesType<T> / ExhaustiveType<T>
Extract enum values and enforce exhaustive handling:
/**
* EnumValuesType / ExhaustiveType — extract enum values and enforce
* exhaustive handling at compile time.
*
* Uses the canonical `PrintStatusSchema` from the bookstore, whose
* enum literally is `'inPrint' | 'outOfPrint'`. The switch must cover
* every literal — `ExhaustiveType<typeof s>` resolves to `never` only
* if no enum members remain, so a missing case surfaces a compile
* error.
*/
import type {
EnumValuesType, ExhaustiveType
} from '../../../src/types/index.js';
import type { PrintStatusSchema } from '../bookstore/index.js';
type PrintStatus = EnumValuesType<typeof PrintStatusSchema>;
function describe(status: PrintStatus): string {
switch (status) {
case 'inPrint':
return 'currently printed';
case 'limitedRun':
return 'limited run';
case 'outOfPrint':
return 'out of print';
default:
return status satisfies ExhaustiveType<typeof status>;
}
}
console.log('inPrint:', describe('inPrint'));
console.log('outOfPrint:', describe('outOfPrint'));
console.log('limitedRun:', describe('limitedRun'));
DefaultAlignedType<T>
Validates that default values match the declared type. Resolves to never when a default mismatches:
/**
* DefaultAlignedType — validates that `default` values match the
* declared type. Resolves to `never` when a default mismatches.
*
* The "Good" schema declares a number default for a number property;
* the "Bad" schema declares a string default for a number property.
* The bad-shape type resolves to `never`, which is how the type
* system surfaces the misalignment without a runtime check.
*/
import type { DefaultAlignedType } from '../../../src/types/index.js';
const _GoodCountSchema = {
'properties': {
'count': {
'default': 0,
'type': 'number'
}
},
'type': 'object'
} as const;
const _BadCountSchema = {
'properties': {
'count': {
'default': 'zero',
'type': 'number'
}
},
'type': 'object'
} as const;
type Good = DefaultAlignedType<typeof _GoodCountSchema>;
type Bad = DefaultAlignedType<typeof _BadCountSchema>;
// Good keeps the schema type; Bad collapses to never.
// The check is entirely compile-time: Good === typeof _GoodCountSchema,
// Bad === never (the string default mismatches the number property type).
type GoodIsSchema = Good extends typeof _GoodCountSchema ? true : false;
type BadIsNever = [Bad] extends [never] ? true : false;
const goodIsSchema: GoodIsSchema = true;
const badIsNever: BadIsNever = true;
console.log('Good matches schema shape (default 0 aligns with number type):', goodIsSchema);
console.log('Bad collapses to never (default "zero" mismatches number type):', badIsNever);
IntegerRangeType<Min, Max> / MultipleOfRangeType<Min, Max, Step>
Manual utilities for generating literal union types from integer ranges:
/**
* IntegerRangeType / MultipleOfRangeType — manual utilities for
* generating literal union types from integer ranges.
*
* Practical for ranges in 0..50. Larger ranges fall back to `number`.
* The bookstore's canonical `RatingScoreSchema` covers `1..5`, so the
* rating literal union is the natural test case.
*/
import type {
IntegerRangeType, MultipleOfRangeType
} from '../../../src/types/index.js';
type Rating = IntegerRangeType<1, 5>;
type EvenDigit = MultipleOfRangeType<0, 8, 2>;
// Both are literal union types at compile time.
// Rating: 1 | 2 | 3 | 4 | 5
// EvenDigit: 0 | 2 | 4 | 6 | 8
const rating: Rating = 4;
const evenDigit: EvenDigit = 6;
console.log('Rating (1 | 2 | 3 | 4 | 5):', rating);
console.log('EvenDigit (0 | 2 | 4 | 6 | 8):', evenDigit);
Practical for ranges in 0-50. Larger ranges fall back to number.
Configuration
All brands are enabled by default. To disable specific categories, create a .d.ts file anywhere in your project's include path (e.g. at the project root or in a types/ directory).
How it works
json-tology exports a JsonTologyTypeConfigInterface with all flags set to true. TypeScript's module augmentation lets you re-open that interface and override specific flags. The compiler merges your declaration with the original, and the type system reads the merged result.
This is the same pattern used by libraries like Zod, tRPC, Express, and Fastify for extensible type configuration.
Setup
Create a file (any name, .d.ts extension) in your project:
// json-tology.d.ts
declare module 'json-tology/types' {
interface JsonTologyTypeConfigInterface {
formatBrands: false; // disable format brands
numericBrands: false; // disable numeric brands
}
}No import needed. No build step. The file just needs to be in your tsconfig's include path.
Available flags
| Flag | Default | Controls |
|---|---|---|
brands | true | Master switch. When false, disables all brands. |
formatBrands | true | format on strings and numbers. |
stringBrands | true | minLength, maxLength, pattern on strings. |
numericBrands | true | minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf on numbers. |
arrayBrands | true | uniqueItems, contains, minItems, maxItems on arrays. |
contentBrands | true | contentMediaType, contentEncoding on strings. |
objectBrands | true | minProperties, maxProperties on objects. additionalProperties: false excess flagging. |
nominalBrands | true | $id nominal identity, $schema dialect branding. |
The master brands flag takes precedence. When brands: false, all other flags are ignored and no brands are applied.
Before and after: format brands
/**
* Format brands — Email resolves to a branded string type.
*
* With `formatBrands: true` (the default), `InferType` on a
* `format: 'email'` schema produces `string & FormatBrandType<'email'>`.
* Plain strings cannot satisfy the branded type — values must come
* through the validation API.
*/
import { JsonTology } from '../../../src/index.js';
import type { InferType } from '../../../src/types/index.js';
const EmailSchema = {
'$id': 'urn:brands:Email',
'format': 'email',
'type': 'string'
} as const;
type Email = InferType<typeof EmailSchema>;
// Email: string & FormatBrandType<'email'>
const jt = JsonTology.create({
'baseIri': 'urn:brands:',
'enableStrictGraph': false,
'schemas': [EmailSchema]
});
const email: Email = jt.instantiate(EmailSchema.$id, 'bastian@bookstore.example');
console.log('Email (format brand):', email);
console.log('Email is string:', typeof email === 'string');
formatBrands | Email resolves to | Plain string assignable? |
|---|---|---|
true (default) | string & FormatBrandInterface<'email'> | No - compile error |
false | string | Yes |
Before and after: numeric brands
/**
* Numeric brands — `minimum` and `maximum` produce branded number types.
*
* With `numericBrands: true` (the default), a schema with `minimum: 0`
* and `maximum: 100` resolves to
* `number & MinimumBrandType<0> & MaximumBrandType<100>`.
* A plain `number` is not assignable to `Score` without passing through
* the validation API.
*/
import { JsonTology } from '../../../src/index.js';
import type { InferType } from '../../../src/types/index.js';
const ScoreSchema = {
'$id': 'urn:brands:Score',
'maximum': 100,
'minimum': 0,
'type': 'number'
} as const;
type Score = InferType<typeof ScoreSchema>;
const jt = JsonTology.create({
'baseIri': 'urn:brands:',
'enableStrictGraph': false,
'schemas': [ScoreSchema]
});
const score: Score = jt.instantiate(ScoreSchema.$id, 75);
console.log('Score (min 0, max 100):', score);
console.log('Score is number:', typeof score === 'number');
numericBrands | Score resolves to | Plain number assignable? |
|---|---|---|
true (default) | number & MinimumBrandInterface<0> & MaximumBrandInterface<100> | No - compile error |
false | number | Yes |
Before and after: string brands
import type { InferType } from '../../../src/types/index.js';
import { JsonTology } from '../../../src/index.js';
const CodeSchema = {
'$id': 'urn:brands:Code',
'maxLength': 10,
'minLength': 3,
'type': 'string'
} as const;
type Code = InferType<typeof CodeSchema>;
// string & MinLengthBrandType<3> & MaxLengthBrandType<10>
const jt = JsonTology.create({
'baseIri': 'urn:brands:',
'enableStrictGraph': false,
'schemas': [CodeSchema]
});
const code: Code = jt.instantiate(CodeSchema.$id, 'ABC');
console.log('Code value (minLength 3, maxLength 10):', code);
// Too-short strings are rejected.
const shortErrors = JsonTology.validate(CodeSchema, 'AB');
console.log('Errors for "AB" (too short):', shortErrors.items.map((err) => {
return err.message;
}));
stringBrands | Code resolves to | Plain string assignable? |
|---|---|---|
true (default) | string & MinLengthBrandInterface<3> & MaxLengthBrandInterface<10> | No - compile error |
false | string | Yes |
Before and after: array brands
/**
* `uniqueItems` array brand — homogeneous arrays with `uniqueItems: true`
* carry `UniqueItemsBrandType`.
*
* A plain `string[]` is not assignable to the branded type. Values must
* pass through the validation API which enforces uniqueness at runtime.
*/
import { JsonTology } from '../../../src/index.js';
import type { InferType } from '../../../src/types/index.js';
const SetSchema = {
'$id': 'urn:brands:StringSet',
'items': { 'type': 'string' },
'type': 'array',
'uniqueItems': true
} as const;
type StringSet = InferType<typeof SetSchema>;
const jt = JsonTology.create({
'baseIri': 'urn:brands:',
'enableStrictGraph': false,
'schemas': [SetSchema]
});
const tags: StringSet = jt.instantiate(SetSchema.$id, [
'fiction',
'fantasy',
'classic'
]);
console.log('StringSet (uniqueItems brand):', tags);
console.log('StringSet is array:', Array.isArray(tags));
arrayBrands | Set resolves to | readonly string[] assignable? |
|---|---|---|
true (default) | readonly string[] & UniqueItemsBrandInterface | No - compile error |
false | readonly string[] | Yes |
Before and after: object brands
/**
* Object brands — `additionalProperties: false` closed object.
*
* With `objectBrands: true` (the default), excess property keys are
* flagged as `never` at compile time when the schema declares both
* `properties` and `additionalProperties: false`.
*/
import type { InferType } from '../../../src/types/index.js';
const _ClosedSchema = {
'additionalProperties': false,
'properties': { 'name': { 'type': 'string' } },
'type': 'object'
} as const;
type Closed = InferType<typeof _ClosedSchema>;
const valid: Closed = { 'name': 'Bastian Balthazar Bux' };
console.log('Closed object:', valid);
console.log('name property:', valid.name);
objectBrands | Excess property { name: 'x', extra: 1 } | Plain object assignable? |
|---|---|---|
true (default) | compile error - extra is never | No |
false | compiles (no excess check) | Yes |
Before and after: all brands off
// json-tology.d.ts - disable everything
declare module 'json-tology/types' {
interface JsonTologyTypeConfigInterface {
brands: false;
}
}All InferType results revert to plain TypeScript types with no phantom brands. The library behaves identically to before brands were introduced. Runtime validation is unaffected.
Checking your config
The augmented interface is type-checked. A typo in a flag name produces a compile error:
declare module 'json-tology/types' {
interface JsonTologyTypeConfigInterface {
formattBrands: false; // compile error - property does not exist
}
}Obtaining branded values
Branded types enforce that data goes through validation. The validation API returns branded types automatically:
/**
* Obtaining branded values — only validation produces them.
*
* Branded types enforce that data goes through validation. The
* canonical EmailSchema in the bookstore carries
* `FormatBrandType<'email'>`; the only way to obtain a value of
* that branded type is to pass through `instantiate`, `value.instantiate`,
* or the type guard `is`.
*/
import {
bookstoreEntities, EmailSchema
} from '../bookstore/index.js';
const candidate = 'bastian.bux@bookstore.example';
const email = bookstoreEntities.instantiate(EmailSchema, candidate);
const echo = bookstoreEntities.value.instantiate(EmailSchema.$id, candidate) as string;
console.log('instantiate produces branded email:', email);
console.log('value.instantiate produces same value:', echo);
if (bookstoreEntities.is(EmailSchema, candidate)) {
// candidate is narrowed to the branded email type inside this branch.
console.log('is() guard narrows to branded type:', candidate);
}
See also
Transform.brand- explicit nominal branding viaBrandOutputType- Type Inference - how
InferTyperesolves brand intersections instantiate- the only source of branded values at runtime- Bookstore domain - branded primitives (
CustomerId,Email,Isbn) - Picking a method - the trust boundary that produces validated, branded values
- Structural Narrowing - patternProperties, integer ranges, if/then, dependentRequired