or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

composite.mdconstraints.mdcontracts.mdindex.mdliterals.mdprimitives.mdresults.mdtemplates.mdunion-intersect.mdutilities.mdvalidation.md

constraints.mddocs/

0

# Constraints and Transformations

1

2

Add custom validation logic and data transformation to existing runtypes. These utilities allow you to extend basic type validation with domain-specific rules, branding, and data parsing.

3

4

## Capabilities

5

6

### Constraint

7

8

Adds custom validation logic to an existing runtype, narrowing its type.

9

10

```typescript { .api }

11

/**

12

* Adds custom validation constraints to a runtype

13

* @param underlying - Base runtype to constrain

14

* @param constraint - Assertion function that validates and narrows the type

15

* @example Constraint(String, s => { if (s.length < 3) throw "Too short"; })

16

*/

17

function Constraint<T, U extends T>(

18

underlying: Runtype<T>,

19

constraint: (x: T) => asserts x is U

20

): ConstraintRuntype<T, U>;

21

22

interface ConstraintRuntype<T, U> extends Runtype<U> {

23

tag: "constraint";

24

underlying: Runtype<T>;

25

constraint: (x: T) => asserts x is U;

26

}

27

```

28

29

**Usage Examples:**

30

31

```typescript

32

import { Constraint, String, Number } from "runtypes";

33

34

// String length constraints

35

const MinLength = (min: number) =>

36

Constraint(String, (s: string): asserts s is string => {

37

if (s.length < min) throw `String must be at least ${min} characters`;

38

});

39

40

const Username = MinLength(3);

41

const username = Username.check("alice"); // "alice"

42

43

// Numeric range constraints

44

const PositiveNumber = Constraint(Number, (n: number): asserts n is number => {

45

if (n <= 0) throw "Number must be positive";

46

});

47

48

const Age = Constraint(Number, (n: number): asserts n is number => {

49

if (n < 0 || n > 150) throw "Age must be between 0 and 150";

50

});

51

52

const age = Age.check(25); // 25

53

54

// Email validation

55

const Email = Constraint(String, (s: string): asserts s is string => {

56

const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

57

if (!emailRegex.test(s)) throw "Invalid email format";

58

});

59

60

const email = Email.check("user@example.com"); // "user@example.com"

61

```

62

63

### Built-in Constraint Helpers

64

65

Use the convenient built-in methods on runtypes for common constraints.

66

67

```typescript

68

import { String, Number } from "runtypes";

69

70

// withConstraint - returns boolean or error message

71

const PositiveInteger = Number.withConstraint(n => n > 0 && Number.isInteger(n) || "Must be positive integer");

72

73

// withGuard - type predicate function

74

const NonEmptyString = String.withGuard((s): s is string => s.length > 0);

75

76

// withAssertion - assertion function

77

const ValidUrl = String.withAssertion((s): asserts s is string => {

78

try {

79

new URL(s);

80

} catch {

81

throw "Invalid URL";

82

}

83

});

84

85

// Usage

86

const count = PositiveInteger.check(5);

87

const name = NonEmptyString.check("Alice");

88

const url = ValidUrl.check("https://example.com");

89

```

90

91

### Brand

92

93

Adds nominal typing to create distinct types that are structurally identical but semantically different.

94

95

```typescript { .api }

96

/**

97

* Adds a brand to create nominal typing

98

* @param brand - Brand identifier string

99

* @param entity - Underlying runtype to brand

100

* @example Brand("UserId", String).check("user_123") // branded string

101

*/

102

function Brand<B extends string, T>(brand: B, entity: Runtype<T>): BrandRuntype<B, T>;

103

104

interface BrandRuntype<B, T> extends Runtype<T & Brand<B>> {

105

tag: "brand";

106

brand: B;

107

entity: Runtype<T>;

108

}

109

110

// Or use the built-in method

111

declare module "runtypes" {

112

interface Runtype<T> {

113

withBrand<B extends string>(brand: B): BrandRuntype<B, T>;

114

}

115

}

116

```

117

118

**Usage Examples:**

119

120

```typescript

121

import { Brand, String, Number } from "runtypes";

122

123

// Create branded types

124

const UserId = Brand("UserId", String);

125

const ProductId = Brand("ProductId", String);

126

const Price = Brand("Price", Number);

127

128

type UserIdType = Static<typeof UserId>; // string & Brand<"UserId">

129

type ProductIdType = Static<typeof ProductId>; // string & Brand<"ProductId">

130

type PriceType = Static<typeof Price>; // number & Brand<"Price">

131

132

// Values are runtime identical but type-distinct

133

const userId = UserId.check("user_123");

134

const productId = ProductId.check("prod_456");

135

const price = Price.check(29.99);

136

137

// Type safety - these would be TypeScript errors:

138

// function processUser(id: UserIdType) { ... }

139

// processUser(productId); // Error: ProductId is not assignable to UserId

140

141

// Using withBrand method

142

const Email = String.withBrand("Email");

143

const Password = String.withBrand("Password");

144

145

// Combining with constraints

146

const ValidatedEmail = String

147

.withConstraint(s => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s) || "Invalid email")

148

.withBrand("Email");

149

150

const StrongPassword = String

151

.withConstraint(s => s.length >= 8 || "Password must be at least 8 characters")

152

.withConstraint(s => /[A-Z]/.test(s) || "Password must contain uppercase letter")

153

.withConstraint(s => /[0-9]/.test(s) || "Password must contain number")

154

.withBrand("StrongPassword");

155

```

156

157

### Parser

158

159

Transforms validated values using a custom parser function, enabling data conversion and normalization.

160

161

```typescript { .api }

162

/**

163

* Adds custom parser to transform validated values

164

* @param underlying - Base runtype for validation

165

* @param parser - Function to transform the validated value

166

* @example Parser(String, s => s.toUpperCase()).parse("hello") // "HELLO"

167

*/

168

function Parser<T, U>(underlying: Runtype<T>, parser: (value: T) => U): ParserRuntype<T, U>;

169

170

interface ParserRuntype<T, U> extends Runtype<U> {

171

tag: "parser";

172

underlying: Runtype<T>;

173

parser: (value: T) => U;

174

}

175

176

// Or use the built-in method

177

declare module "runtypes" {

178

interface Runtype<T> {

179

withParser<U>(parser: (value: T) => U): ParserRuntype<T, U>;

180

}

181

}

182

```

183

184

**Usage Examples:**

185

186

```typescript

187

import { Parser, String, Number, Array, Object } from "runtypes";

188

189

// String transformations

190

const UpperCaseString = Parser(String, s => s.toUpperCase());

191

const TrimmedString = Parser(String, s => s.trim());

192

193

const name = UpperCaseString.parse("alice"); // "ALICE"

194

const cleaned = TrimmedString.parse(" hello "); // "hello"

195

196

// Numeric transformations

197

const IntegerFromString = Parser(String, s => parseInt(s, 10));

198

const AbsoluteNumber = Parser(Number, n => Math.abs(n));

199

200

const parsed = IntegerFromString.parse("123"); // 123 (number)

201

const absolute = AbsoluteNumber.parse(-42); // 42

202

203

// Date parsing

204

const DateFromString = Parser(String, s => new Date(s));

205

const ISODateFromTimestamp = Parser(Number, n => new Date(n).toISOString());

206

207

const date = DateFromString.parse("2024-01-15"); // Date object

208

const isoString = ISODateFromTimestamp.parse(1642204800000); // "2022-01-15T00:00:00.000Z"

209

210

// Complex object transformation

211

const UserInput = Object({

212

firstName: String,

213

lastName: String,

214

age: String, // Input as string

215

tags: String // Comma-separated string

216

});

217

218

const User = Parser(UserInput, input => ({

219

fullName: `${input.firstName} ${input.lastName}`,

220

age: parseInt(input.age, 10),

221

tags: input.tags.split(",").map(tag => tag.trim()),

222

createdAt: new Date()

223

}));

224

225

const userData = User.parse({

226

firstName: "Alice",

227

lastName: "Smith",

228

age: "25",

229

tags: "developer, typescript, nodejs"

230

});

231

// {

232

// fullName: "Alice Smith",

233

// age: 25,

234

// tags: ["developer", "typescript", "nodejs"],

235

// createdAt: Date

236

// }

237

```

238

239

### Chaining Constraints and Transformations

240

241

```typescript

242

import { String, Number } from "runtypes";

243

244

// Chain multiple operations

245

const ProcessedUsername = String

246

.withConstraint(s => s.length >= 3 || "Username too short")

247

.withParser(s => s.toLowerCase())

248

.withParser(s => s.replace(/[^a-z0-9]/g, ""))

249

.withConstraint(s => s.length > 0 || "Username cannot be empty after processing")

250

.withBrand("Username");

251

252

const username = ProcessedUsername.parse("Alice_123!"); // "alice123" (branded)

253

254

// Numeric processing chain

255

const Currency = Number

256

.withConstraint(n => n >= 0 || "Amount cannot be negative")

257

.withParser(n => Math.round(n * 100) / 100) // Round to 2 decimal places

258

.withBrand("Currency");

259

260

const price = Currency.parse(19.999); // 20.00 (branded)

261

```

262

263

## Advanced Constraint Patterns

264

265

### Custom Validation with Context

266

267

```typescript

268

import { Constraint, String, Object } from "runtypes";

269

270

// Validation that depends on other fields

271

const UserRegistration = Object({

272

username: String,

273

password: String,

274

confirmPassword: String

275

}).withConstraint((user): asserts user is any => {

276

if (user.password !== user.confirmPassword) {

277

throw "Passwords do not match";

278

}

279

if (user.password.includes(user.username)) {

280

throw "Password cannot contain username";

281

}

282

});

283

284

// Conditional validation

285

const ConditionalField = Object({

286

type: Union(Literal("premium"), Literal("basic")),

287

features: Array(String)

288

}).withConstraint((obj): asserts obj is any => {

289

if (obj.type === "premium" && obj.features.length === 0) {

290

throw "Premium type must have at least one feature";

291

}

292

});

293

```

294

295

### Dynamic Constraints

296

297

```typescript

298

import { Constraint, String, Number } from "runtypes";

299

300

// Factory for creating range constraints

301

const createRange = (min: number, max: number) =>

302

Number.withConstraint(n =>

303

(n >= min && n <= max) || `Must be between ${min} and ${max}`

304

);

305

306

const Percentage = createRange(0, 100);

307

const Rating = createRange(1, 5);

308

309

// Factory for string patterns

310

const createPattern = (pattern: RegExp, message: string) =>

311

String.withConstraint(s => pattern.test(s) || message);

312

313

const PhoneNumber = createPattern(/^\+?[\d\s-()]+$/, "Invalid phone number format");

314

const ZipCode = createPattern(/^\d{5}(-\d{4})?$/, "Invalid ZIP code format");

315

```

316

317

### Error Handling in Constraints

318

319

```typescript

320

import { Constraint, String, ValidationError } from "runtypes";

321

322

// Custom error types

323

class ValidationFailure extends Error {

324

constructor(public field: string, public value: unknown, message: string) {

325

super(message);

326

this.name = "ValidationFailure";

327

}

328

}

329

330

const StrictEmail = Constraint(String, (s: string): asserts s is string => {

331

const parts = s.split("@");

332

if (parts.length !== 2) {

333

throw new ValidationFailure("email", s, "Email must contain exactly one @ symbol");

334

}

335

336

const [local, domain] = parts;

337

if (!local || local.length === 0) {

338

throw new ValidationFailure("email.local", local, "Email local part cannot be empty");

339

}

340

341

if (!domain || !domain.includes(".")) {

342

throw new ValidationFailure("email.domain", domain, "Email domain must contain a dot");

343

}

344

});

345

346

// Usage with error handling

347

try {

348

const email = StrictEmail.check("invalid-email");

349

} catch (error) {

350

if (error instanceof ValidationError) {

351

console.log("Validation failed:", error.failure.message);

352

} else if (error instanceof ValidationFailure) {

353

console.log(`Field ${error.field} failed:`, error.message);

354

}

355

}

356

```