A collection of codecs and combinators for use with io-ts
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Codecs for working with arrays, sets, and maps including non-empty arrays and proper serialization of complex collection types. These codecs enable type-safe handling of collections with specialized constraints and serialization formats.
Creates codecs for fp-ts NonEmptyArray types that ensure arrays always contain at least one element.
/**
* Type interface for NonEmptyArray codec
*/
interface NonEmptyArrayC<C extends t.Mixed>
extends t.Type<NonEmptyArray<t.TypeOf<C>>, NonEmptyArray<t.OutputOf<C>>, unknown> {}
/**
* Creates a codec for NonEmptyArray<A> that validates arrays are non-empty
* @param codec - The codec for individual array elements
* @param name - Optional name for the codec
* @returns NonEmptyArray codec that ensures at least one element
*/
function nonEmptyArray<C extends t.Mixed>(
codec: C,
name?: string
): NonEmptyArrayC<C>;Usage Examples:
import { nonEmptyArray } from "io-ts-types";
import * as t from "io-ts";
import { NonEmptyArray } from "fp-ts/lib/NonEmptyArray";
// Create codec for non-empty array of strings
const NonEmptyStringArray = nonEmptyArray(t.string);
const result1 = NonEmptyStringArray.decode(["hello", "world"]);
// Right(NonEmptyArray<string>)
const result2 = NonEmptyStringArray.decode(["single"]);
// Right(NonEmptyArray<string>)
const result3 = NonEmptyStringArray.decode([]);
// Left([ValidationError]) - empty array not allowed
const result4 = NonEmptyStringArray.decode(["hello", 123]);
// Left([ValidationError]) - invalid element type
// Encoding back to regular array
const nonEmptyArray = result1.right;
const encoded = NonEmptyStringArray.encode(nonEmptyArray);
// ["hello", "world"]Creates codecs for readonly non-empty arrays, providing immutable collection guarantees.
/**
* Interface for readonly non-empty arrays
*/
interface ReadonlyNonEmptyArray<A> extends ReadonlyArray<A> {
readonly 0: A;
}
/**
* Type interface for ReadonlyNonEmptyArray codec
*/
interface ReadonlyNonEmptyArrayC<C extends t.Mixed>
extends t.Type<ReadonlyNonEmptyArray<t.TypeOf<C>>, ReadonlyNonEmptyArray<t.OutputOf<C>>, unknown> {}
/**
* Creates a codec for ReadonlyNonEmptyArray<A>
* @param codec - The codec for individual array elements
* @param name - Optional name for the codec
* @returns ReadonlyNonEmptyArray codec
*/
function readonlyNonEmptyArray<C extends t.Mixed>(
codec: C,
name?: string
): ReadonlyNonEmptyArrayC<C>;Usage Examples:
import { readonlyNonEmptyArray } from "io-ts-types";
import * as t from "io-ts";
const ReadonlyNumbers = readonlyNonEmptyArray(t.number);
const result1 = ReadonlyNumbers.decode([1, 2, 3]);
// Right(ReadonlyNonEmptyArray<number>)
const result2 = ReadonlyNumbers.decode([]);
// Left([ValidationError]) - empty array
// The result is readonly - no mutation methods available
if (result1._tag === "Right") {
const numbers = result1.right;
const first = numbers[0]; // 1 (guaranteed to exist)
// numbers.push(4); // TypeScript error - readonly array
}Creates codecs for Set types that serialize as arrays, with uniqueness validation and ordering support.
/**
* Type interface for Set codec
*/
interface SetFromArrayC<C extends t.Mixed>
extends t.Type<Set<t.TypeOf<C>>, Array<t.OutputOf<C>>, unknown> {}
/**
* Creates a codec for Set<A> that serializes as an array
* @param codec - The codec for individual set elements
* @param O - Ord instance for element comparison and ordering
* @param name - Optional name for the codec
* @returns Set codec that validates uniqueness and requires Ord instance
*/
function setFromArray<C extends t.Mixed>(
codec: C,
O: Ord<t.TypeOf<C>>,
name?: string
): SetFromArrayC<C>;Usage Examples:
import { setFromArray } from "io-ts-types";
import * as t from "io-ts";
import { Ord } from "fp-ts/lib/Ord";
import * as S from "fp-ts/lib/string";
import * as N from "fp-ts/lib/number";
// String set with string ordering
const StringSet = setFromArray(t.string, S.Ord);
const result1 = StringSet.decode(["apple", "banana", "cherry"]);
// Right(Set<string>)
const result2 = StringSet.decode(["apple", "banana", "apple"]);
// Right(Set<string>) - duplicates removed
const result3 = StringSet.decode([]);
// Right(Set<string>) - empty set is valid
// Number set with number ordering
const NumberSet = setFromArray(t.number, N.Ord);
const result4 = NumberSet.decode([3, 1, 4, 1, 5]);
// Right(Set<number>) - duplicates removed, order determined by Ord
// Encoding back to array (ordered by Ord)
const stringSet = new Set(["zebra", "apple", "banana"]);
const encoded = StringSet.encode(stringSet);
// ["apple", "banana", "zebra"] - sorted by string OrdCreates codecs for ReadonlySet types with immutable guarantees.
/**
* Type interface for ReadonlySet codec
*/
interface ReadonlySetFromArrayC<C extends t.Mixed>
extends t.Type<ReadonlySet<t.TypeOf<C>>, ReadonlyArray<t.OutputOf<C>>, unknown> {}
/**
* Creates a codec for ReadonlySet<A>
* @param codec - The codec for individual set elements
* @param O - Ord instance for element comparison and ordering
* @param name - Optional name for the codec
* @returns ReadonlySet codec
*/
function readonlySetFromArray<C extends t.Mixed>(
codec: C,
O: Ord<t.TypeOf<C>>,
name?: string
): ReadonlySetFromArrayC<C>;Creates codecs for Map types that serialize as arrays of key-value pairs.
/**
* Type interface for Map codec
*/
interface MapFromEntriesC<K extends t.Mixed, V extends t.Mixed>
extends t.Type<Map<t.TypeOf<K>, t.TypeOf<V>>, Array<[t.OutputOf<K>, t.OutputOf<V>]>, unknown> {}
/**
* Creates a codec for Map<K, V> that serializes as an array of key-value pairs
* @param keyCodec - The codec for map keys
* @param KO - Ord instance for key comparison and ordering
* @param valueCodec - The codec for map values
* @param name - Optional name for the codec
* @returns Map codec that requires Ord instance for keys
*/
function mapFromEntries<K extends t.Mixed, V extends t.Mixed>(
keyCodec: K,
KO: Ord<t.TypeOf<K>>,
valueCodec: V,
name?: string
): MapFromEntriesC<K, V>;Usage Examples:
import { mapFromEntries } from "io-ts-types";
import * as t from "io-ts";
import * as S from "fp-ts/lib/string";
import * as N from "fp-ts/lib/number";
// Map with string keys and number values
const StringNumberMap = mapFromEntries(t.string, S.Ord, t.number);
const result1 = StringNumberMap.decode([
["apple", 5],
["banana", 3],
["cherry", 8]
]);
// Right(Map<string, number>)
const result2 = StringNumberMap.decode([]);
// Right(Map<string, number>) - empty map is valid
const result3 = StringNumberMap.decode([
["apple", 5],
["apple", 10] // Duplicate key - last value wins
]);
// Right(Map<string, number>) - Map with apple -> 10
// Invalid value type
const result4 = StringNumberMap.decode([
["apple", "not-a-number"]
]);
// Left([ValidationError])
// Encoding back to entries array
const map = new Map([["zebra", 1], ["apple", 2], ["banana", 3]]);
const encoded = StringNumberMap.encode(map);
// [["apple", 2], ["banana", 3], ["zebra", 1]] - sorted by key OrdCreates codecs for ReadonlyMap types with immutable guarantees.
/**
* Type interface for ReadonlyMap codec
*/
interface ReadonlyMapFromEntriesC<K extends t.Mixed, V extends t.Mixed>
extends t.Type<ReadonlyMap<t.TypeOf<K>, t.TypeOf<V>>, ReadonlyArray<[t.OutputOf<K>, t.OutputOf<V>]>, unknown> {}
/**
* Creates a codec for ReadonlyMap<K, V>
* @param keyCodec - The codec for map keys
* @param KO - Ord instance for key comparison and ordering
* @param valueCodec - The codec for map values
* @param name - Optional name for the codec
* @returns ReadonlyMap codec
*/
function readonlyMapFromEntries<K extends t.Mixed, V extends t.Mixed>(
keyCodec: K,
KO: Ord<t.TypeOf<K>>,
valueCodec: V,
name?: string
): ReadonlyMapFromEntriesC<K, V>;import * as t from "io-ts";
import { nonEmptyArray, setFromArray, mapFromEntries } from "io-ts-types";
import * as S from "fp-ts/lib/string";
import * as N from "fp-ts/lib/number";
const User = t.type({
id: t.number,
name: t.string,
email: t.string
});
const Project = t.type({
id: t.number,
name: t.string,
members: nonEmptyArray(User), // At least one member required
tags: setFromArray(t.string, S.Ord), // Unique tags
metadata: mapFromEntries(t.string, S.Ord, t.string) // Key-value metadata
});
const projectData = {
id: 1,
name: "Web Application",
members: [
{ id: 1, name: "Alice", email: "alice@example.com" },
{ id: 2, name: "Bob", email: "bob@example.com" }
],
tags: ["typescript", "react", "nodejs", "react"], // Duplicate removed
metadata: [
["repository", "https://github.com/org/project"],
["status", "active"],
["priority", "high"]
]
};
const parsed = Project.decode(projectData);
// Right({
// id: 1,
// name: "Web Application",
// members: NonEmptyArray<User>,
// tags: Set<string>,
// metadata: Map<string, string>
// })import * as t from "io-ts";
import { setFromArray, mapFromEntries, nonEmptyArray } from "io-ts-types";
import * as S from "fp-ts/lib/string";
import * as N from "fp-ts/lib/number";
const ServerConfig = t.type({
hosts: nonEmptyArray(t.string), // At least one host
allowedOrigins: setFromArray(t.string, S.Ord), // Unique origins
ports: setFromArray(t.number, N.Ord), // Unique ports
environment: mapFromEntries(t.string, S.Ord, t.string) // Env variables
});
const config = {
hosts: ["api.example.com", "api2.example.com"],
allowedOrigins: ["https://app.example.com", "https://admin.example.com"],
ports: [8080, 8443, 9000],
environment: [
["NODE_ENV", "production"],
["LOG_LEVEL", "info"],
["DATABASE_URL", "postgresql://..."]
]
};
const validated = ServerConfig.decode(config);
// Right({ hosts: NonEmptyArray, allowedOrigins: Set, ports: Set, environment: Map })import * as t from "io-ts";
import { nonEmptyArray, setFromArray } from "io-ts-types";
import * as S from "fp-ts/lib/string";
import { pipe } from "fp-ts/lib/function";
import * as NEA from "fp-ts/lib/NonEmptyArray";
const ProcessingJob = t.type({
id: t.string,
inputFiles: nonEmptyArray(t.string), // Must have input files
outputFormats: setFromArray(t.string, S.Ord), // Unique output formats
steps: nonEmptyArray(t.string) // Must have processing steps
});
const jobData = {
id: "job_001",
inputFiles: ["data.csv", "metadata.json"],
outputFormats: ["json", "csv", "xml", "json"], // Duplicate removed
steps: ["validate", "transform", "aggregate", "export"]
};
const job = ProcessingJob.decode(jobData);
if (job._tag === "Right") {
const { inputFiles, outputFormats, steps } = job.right;
// Safe operations on non-empty arrays
const firstFile = NEA.head(inputFiles); // "data.csv"
const totalSteps = steps.length; // 4
// Safe operations on sets
const hasJsonOutput = outputFormats.has("json"); // true
const formatCount = outputFormats.size; // 3 (duplicates removed)
console.log(`Processing ${totalSteps} steps for ${inputFiles.length} files`);
console.log(`Generating ${formatCount} output formats`);
}import * as t from "io-ts";
import { nonEmptyArray, setFromArray, mapFromEntries } from "io-ts-types";
import * as S from "fp-ts/lib/string";
import * as N from "fp-ts/lib/number";
const Category = t.type({
id: t.number,
name: t.string
});
const Article = t.type({
id: t.number,
title: t.string,
content: t.string,
categories: nonEmptyArray(Category), // Must be in at least one category
tags: setFromArray(t.string, S.Ord), // Unique tags
metadata: mapFromEntries(t.string, S.Ord, t.string), // Custom metadata
relatedArticles: setFromArray(t.number, N.Ord) // Unique article IDs
});
const articleData = {
id: 1,
title: "Getting Started with TypeScript",
content: "TypeScript is a typed superset of JavaScript...",
categories: [
{ id: 1, name: "Programming" },
{ id: 2, name: "TypeScript" }
],
tags: ["typescript", "javascript", "tutorial", "beginners"],
metadata: [
["author", "Alice Developer"],
["publishDate", "2023-12-25"],
["readTime", "10 minutes"]
],
relatedArticles: [5, 12, 8, 15]
};
const parsed = Article.decode(articleData);
// Right({
// id: 1,
// title: "...",
// content: "...",
// categories: NonEmptyArray<Category>,
// tags: Set<string>,
// metadata: Map<string, string>,
// relatedArticles: Set<number>
// })import * as t from "io-ts";
import { nonEmptyArray, setFromArray } from "io-ts-types";
import * as S from "fp-ts/lib/string";
const SurveyResponse = t.type({
respondentId: t.string,
answers: nonEmptyArray(t.string), // Must answer at least one question
selectedOptions: setFromArray(t.string, S.Ord), // Unique selected options
feedback: t.union([t.string, t.null])
});
// Form data with repeated selections
const responseData = {
respondentId: "user_123",
answers: ["Satisfied", "Very Good", "Yes"],
selectedOptions: ["option_a", "option_c", "option_b", "option_a"], // Duplicates
feedback: "Great survey!"
};
const validated = SurveyResponse.decode(responseData);
if (validated._tag === "Right") {
const response = validated.right;
// selectedOptions is now a Set with duplicates removed
const uniqueSelections = Array.from(response.selectedOptions);
// ["option_a", "option_b", "option_c"] - sorted by string Ord
console.log(`Respondent ${response.respondentId} provided ${response.answers.length} answers`);
console.log(`Selected ${response.selectedOptions.size} unique options`);
}