or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

codec.mdcombinators.mdcore-types.mddecoder.mdencoder.mdindex.mdinfrastructure.mdprimitives.mdrefinement.mdreporters.mdschema.mdtask-decoder.mdvalidation.md
tile.json

refinement.mddocs/

0

# Refinement & Branding

1

2

Advanced type construction for adding runtime constraints and creating branded types.

3

4

## Capabilities

5

6

### Refinement Function

7

8

Add runtime constraints to existing codecs with custom predicate functions.

9

10

```typescript { .api }

11

/**

12

* Add a refinement predicate to an existing codec

13

* @param codec - The base codec to refine

14

* @param predicate - Predicate function for additional validation

15

* @param name - Optional name for the refined codec

16

*/

17

function refinement<C extends Any, B extends TypeOf<C>>(

18

codec: C,

19

refinement: Refinement<TypeOf<C>, B>,

20

name?: string

21

): RefinementC<C, B>;

22

23

function refinement<C extends Any>(

24

codec: C,

25

predicate: Predicate<TypeOf<C>>,

26

name?: string

27

): RefinementC<C>;

28

29

interface RefinementC<C extends Any, B = TypeOf<C>> extends RefinementType<C, B, OutputOf<C>, InputOf<C>> {}

30

31

/** Predicate function type */

32

type Predicate<A> = (a: A) => boolean;

33

34

/** Refinement function type */

35

type Refinement<A, B extends A> = (a: A) => a is B;

36

```

37

38

**Usage Examples:**

39

40

```typescript

41

import * as t from "io-ts";

42

43

// Create a positive number codec

44

const PositiveNumber = t.refinement(

45

t.number,

46

(n): n is number => n > 0,

47

'PositiveNumber'

48

);

49

50

const result1 = PositiveNumber.decode(42);

51

// result: Right(42)

52

53

const result2 = PositiveNumber.decode(-5);

54

// result: Left([validation error])

55

56

// Create an email codec using string refinement

57

const Email = t.refinement(

58

t.string,

59

(s): s is string => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s),

60

'Email'

61

);

62

63

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

64

// result: Right("user@example.com")

65

66

// Create non-empty array codec

67

const NonEmptyArray = <A>(codec: t.Type<A>) =>

68

t.refinement(

69

t.array(codec),

70

(arr): arr is [A, ...A[]] => arr.length > 0,

71

`NonEmptyArray<${codec.name}>`

72

);

73

74

const NonEmptyStrings = NonEmptyArray(t.string);

75

const result3 = NonEmptyStrings.decode(["hello"]);

76

// result: Right(["hello"])

77

78

const result4 = NonEmptyStrings.decode([]);

79

// result: Left([validation error])

80

```

81

82

### Brand Function

83

84

Create branded types that are distinct at the type level while sharing runtime representation.

85

86

```typescript { .api }

87

/**

88

* Create a branded codec that adds a compile-time brand to values

89

* @param codec - The base codec to brand

90

* @param predicate - Refinement predicate for the brand

91

* @param name - Brand name (becomes part of the type)

92

*/

93

function brand<C extends Any, N extends string, B extends { readonly [K in N]: symbol }>(

94

codec: C,

95

predicate: Refinement<TypeOf<C>, Branded<TypeOf<C>, B>>,

96

name: N

97

): BrandC<C, B>;

98

99

interface BrandC<C extends Any, B> extends RefinementType<C, Branded<TypeOf<C>, B>, OutputOf<C>, InputOf<C>> {}

100

101

/** Brand interface for creating branded types */

102

interface Brand<B> {

103

readonly [_brand]: B;

104

}

105

106

/** Branded type combining base type with brand */

107

type Branded<A, B> = A & Brand<B>;

108

```

109

110

**Usage Examples:**

111

112

```typescript

113

import * as t from "io-ts";

114

115

// Create a UserId brand

116

interface UserIdBrand {

117

readonly UserId: unique symbol;

118

}

119

120

const UserId = t.brand(

121

t.number,

122

(n): n is t.Branded<number, UserIdBrand> => Number.isInteger(n) && n > 0,

123

'UserId'

124

);

125

126

type UserId = t.TypeOf<typeof UserId>;

127

// UserId = number & Brand<UserIdBrand>

128

129

const userId = UserId.decode(123);

130

// result: Right(123) with UserId brand

131

132

// Create an Email brand

133

interface EmailBrand {

134

readonly Email: unique symbol;

135

}

136

137

const EmailBranded = t.brand(

138

t.string,

139

(s): s is t.Branded<string, EmailBrand> => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s),

140

'Email'

141

);

142

143

type Email = t.TypeOf<typeof EmailBranded>;

144

// Email = string & Brand<EmailBrand>

145

146

// Branded types prevent accidental mixing

147

function sendEmail(to: Email, from: UserId) {

148

// Implementation

149

}

150

151

const email = EmailBranded.decode("user@example.com");

152

const id = UserId.decode(456);

153

154

if (email._tag === "Right" && id._tag === "Right") {

155

sendEmail(email.right, id.right); // Type safe

156

// sendEmail(id.right, email.right); // Type error!

157

}

158

```

159

160

### Built-in Integer Brand

161

162

Pre-defined integer codec using the branding system.

163

164

```typescript { .api }

165

/** Integer brand interface */

166

interface IntBrand {

167

readonly Int: unique symbol;

168

}

169

170

/** Branded integer type */

171

type Int = Branded<number, IntBrand>;

172

173

/** Integer codec that validates integer numbers */

174

const Int: BrandC<NumberC, IntBrand>;

175

176

/** Pre-defined Integer codec (deprecated, use Int) */

177

const Integer: RefinementC<NumberC>;

178

```

179

180

**Usage Example:**

181

182

```typescript

183

import * as t from "io-ts";

184

185

const result1 = t.Int.decode(42);

186

// result: Right(42) with Int brand

187

188

const result2 = t.Int.decode(42.5);

189

// result: Left([validation error - not an integer])

190

191

const result3 = t.Int.decode("42");

192

// result: Left([validation error - not a number])

193

194

// Type-level distinction

195

function processInt(value: t.Int) {

196

// value is guaranteed to be an integer

197

return value * 2;

198

}

199

200

if (result1._tag === "Right") {

201

const doubled = processInt(result1.right); // Type safe

202

}

203

```

204

205

## Advanced Usage Patterns

206

207

### Multiple Refinements

208

209

```typescript

210

import * as t from "io-ts";

211

212

// Chain multiple refinements

213

const PositiveEvenNumber = t.refinement(

214

t.refinement(

215

t.number,

216

(n): n is number => n > 0,

217

'PositiveNumber'

218

),

219

(n): n is number => n % 2 === 0,

220

'EvenNumber'

221

);

222

223

const result = PositiveEvenNumber.decode(10);

224

// result: Right(10) - positive and even

225

226

const invalid1 = PositiveEvenNumber.decode(-2);

227

// result: Left([validation error - not positive])

228

229

const invalid2 = PositiveEvenNumber.decode(3);

230

// result: Left([validation error - not even])

231

```

232

233

### Custom Branded Types

234

235

```typescript

236

import * as t from "io-ts";

237

238

// Create a URL brand

239

interface UrlBrand {

240

readonly Url: unique symbol;

241

}

242

243

const Url = t.brand(

244

t.string,

245

(s): s is t.Branded<string, UrlBrand> => {

246

try {

247

new URL(s);

248

return true;

249

} catch {

250

return false;

251

}

252

},

253

'Url'

254

);

255

256

type Url = t.TypeOf<typeof Url>;

257

258

// Create a non-empty string brand

259

interface NonEmptyStringBrand {

260

readonly NonEmptyString: unique symbol;

261

}

262

263

const NonEmptyString = t.brand(

264

t.string,

265

(s): s is t.Branded<string, NonEmptyStringBrand> => s.length > 0,

266

'NonEmptyString'

267

);

268

269

type NonEmptyString = t.TypeOf<typeof NonEmptyString>;

270

271

// Use branded types in complex structures

272

const UserProfile = t.type({

273

name: NonEmptyString,

274

website: Url,

275

id: t.Int

276

});

277

278

type UserProfile = t.TypeOf<typeof UserProfile>;

279

```

280

281

### Range-based Refinements

282

283

```typescript

284

import * as t from "io-ts";

285

286

// Create range validation

287

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

288

t.refinement(

289

t.number,

290

(n): n is number => n >= min && n <= max,

291

`Range(${min}, ${max})`

292

);

293

294

const Age = range(0, 150);

295

const Percentage = range(0, 100);

296

const Temperature = range(-273.15, Infinity);

297

298

const User = t.type({

299

name: t.string,

300

age: Age,

301

score: Percentage

302

});

303

304

const result = User.decode({

305

name: "Alice",

306

age: 25,

307

score: 95

308

});

309

// result: Right({ name: "Alice", age: 25, score: 95 })

310

311

const invalid = User.decode({

312

name: "Bob",

313

age: 200, // Too old

314

score: 110 // Over 100%

315

});

316

// result: Left([validation errors for age and score])

317

```

318

319

### String Pattern Refinements

320

321

```typescript

322

import * as t from "io-ts";

323

324

// Create pattern-based string codecs

325

const pattern = (regex: RegExp, name: string) =>

326

t.refinement(

327

t.string,

328

(s): s is string => regex.test(s),

329

name

330

);

331

332

const PhoneNumber = pattern(/^\+?[\d\s()-]+$/, 'PhoneNumber');

333

const PostalCode = pattern(/^\d{5}(-\d{4})?$/, 'PostalCode');

334

const HexColor = pattern(/^#[0-9A-Fa-f]{6}$/, 'HexColor');

335

336

const ContactInfo = t.type({

337

phone: PhoneNumber,

338

zip: PostalCode,

339

favoriteColor: HexColor

340

});

341

342

const result = ContactInfo.decode({

343

phone: "+1 (555) 123-4567",

344

zip: "12345",

345

favoriteColor: "#FF0000"

346

});

347

// result: Right(contact info)

348

```

349

350

### Combining Brands and Refinements

351

352

```typescript

353

import * as t from "io-ts";

354

355

// Create a sophisticated ID system

356

interface ProductIdBrand {

357

readonly ProductId: unique symbol;

358

}

359

360

interface CategoryIdBrand {

361

readonly CategoryId: unique symbol;

362

}

363

364

const ProductId = t.brand(

365

t.refinement(

366

t.string,

367

(s): s is string => s.startsWith('PROD-') && s.length === 10,

368

'ProductIdFormat'

369

),

370

(s): s is t.Branded<string, ProductIdBrand> => true,

371

'ProductId'

372

);

373

374

const CategoryId = t.brand(

375

t.refinement(

376

t.string,

377

(s): s is string => s.startsWith('CAT-') && s.length === 9,

378

'CategoryIdFormat'

379

),

380

(s): s is t.Branded<string, CategoryIdBrand> => true,

381

'CategoryId'

382

);

383

384

type ProductId = t.TypeOf<typeof ProductId>;

385

type CategoryId = t.TypeOf<typeof CategoryId>;

386

387

// These types are now completely distinct

388

function assignCategory(productId: ProductId, categoryId: CategoryId) {

389

// Implementation

390

}

391

392

const prodId = ProductId.decode("PROD-12345");

393

const catId = CategoryId.decode("CAT-54321");

394

395

if (prodId._tag === "Right" && catId._tag === "Right") {

396

assignCategory(prodId.right, catId.right); // Type safe

397

// assignCategory(catId.right, prodId.right); // Type error!

398

}

399

```