Styp (Sum TYPes) is written in Javascript, the library provides mechanisms for creating constructors (for product types) and sum types (also known as disjoint union types or tagged unions). This library has been inspired by languages like Haskell, F# and OCaml which provide sum types natively. Styp has also taken inspiration from daggy (a library for creating sum types in Javascript).
import { tagged, sum, match } from "styp"; // Or: const { tagged, sum, match } = require("styp");
// Product Type (like a struct or record)
const Polar = tagged("Polar", ["r", "theta"]);
let p1 = Polar(3, 0.88);
console.log(p1.toString()); // -> Polar(3,0.88)
console.log(p1.unwrap()); // -> { $type: "Polar", r: 3, theta: 0.88 }
// Sum Type (Tagged Union)
const Maybe = sum("Maybe", {
Just: ["val"],
Nothing: []
});
let testJust = Maybe.Just(10);
let testNothing = Maybe.Nothing;
console.log(testJust.toString()); // -> Maybe.Just(10)
console.log(Maybe.is(testJust)); // -> true
console.log(Maybe.Just.is(testJust)); // -> true
console.log(Maybe.Nothing.is(testJust)); // -> false
console.log(testNothing.unwrap()); // -> { $type: "Nothing" }
// Using cata for matching
let value = testJust.cata({
Just: ({ val }) => val,
Nothing: () => 0
});
console.log(value * 5); // -> 50
// Using the match utility for matching
const getValue = match(Maybe)({
Just: ({ val }) => val,
Nothing: () => 0
});
console.log(getValue(testNothing) + 5); // -> 5npm i styp<script src="https://unpkg.com/styp"></script>Or from JSDelivr:
<script src="https://cdn.jsdelivr.net/npm/styp"></script>Evolving!
⚠️ Note: This doc tracks an unreleased update, published version may differ.
This function creates a constructor for a "product type" (a type with a fixed set of named fields).
typename: A string representing the name of the type.fields: An array of strings, where each string is a field name for the type.structural(optional, default:false): A boolean flag to determine typing behavior forinstanceofchecks.false(default): Nominal typing.true: Structural typing. (See "Typing: Nominal vs. Structural" section for details.)
Returns:
- If
fieldsis not empty, it returns a constructor function. - If
fieldsis empty, it returns an immutable singleton object representing a type with only one possible value.
import { tagged } from "styp";
// Constructor for a Point type
const Point = tagged("Point", ["x", "y"]);
const p1 = Point(10, 20);
console.log(p1.x); // -> 10
console.log(p1 instanceof Point); // -> true
// Singleton type (e.g., for a custom null or unit type)
const Nil = tagged("Nil", []);
let temp = Nil;
console.log(Nil.is(temp)); // -> true
console.log(temp.toString()); // -> "Nil"
console.log(Nil.unwrap()); // -> { $type: "Nil" }
// Structural Point type
const StructPoint = tagged("StructPoint", ["x", "y"], true);
const sp1 = StructPoint(5, 15);
console.log(sp1 instanceof StructPoint); // -> true
console.log({x:1, y:2, z:3} instanceof StructPoint); // -> true (has x and y)
const AnyObjNil = tagged("AnyObjNil", [], true); // Structural singleton
console.log({} instanceof AnyObjNil); // -> true (any object is an instance)
console.log(AnyObjNil.is({})); // -> true (any object is an instance)Instances created by tagged constructors are immutable (frozen with Object.freeze).
This function creates a "sum type" (or tagged union), which is a type that can take on one of several distinct forms (variants), each with its own potential fields.
typename: A string representing the name of the sum type.constructors: An object where:- Keys are strings representing the names of the variant constructors (e.g., "Some", "None").
- Values are arrays of strings, representing the field names for that specific variant. An empty array
[]means the variant has no fields.
structural(optional, default:false): A boolean flag passed down to its variant constructors, influencing theirinstanceofbehavior and consequently the sum type'sinstanceofbehavior. (See "Typing: Nominal vs. Structural" section.)
Returns: An object that acts as a namespace for the sum type and its variant constructors.
import { sum } from "styp";
const Result = sum("Result", {
Ok: ["data"], // Variant 'Ok' with one field 'data'
Err: ["message"] // Variant 'Err' with one field 'message'
});
const success = Result.Ok("Everything went well!");
const failure = Result.Err("Something broke.");
console.log(success.toString()); // -> Result.Ok(Everything went well!)
console.log(Result.Ok.is(success)); // -> true
console.log(failue instanceof Result.Err); // -> true
console.log(Result.is(failure)); // -> true (it's an instance of the Result sum type)
console.log(failure.unwrap("kind")); // -> { kind: "Err", message: "Something broke." }
const HttpMethod = sum("HttpMethod", {
GET: [],
POST: [],
PUT: [],
DELETE: []
});
const method = HttpMethod.GET; // 'GET' is a singleton variant
console.log(HttpMethod.GET.toString()); // -> HttpMethod.GET
// Structural sum type
const StructResult = sum("StructResult", { Ok: ["data"], Err: ["message"] }, true);
const structErrObj = { message: "Structural error" }; // A plain object
console.log(structErrObj instanceof StructResult.Err); // -> true
console.log(structErrObj instanceof StructResult); // -> trueEach variant constructor (e.g., Result.Ok) behaves like a type created by tagged(). Instances of variants are also immutable.
Provides a functional approach for pattern matching on instances of a sum type. It helps ensure that all cases (variants) of a sum type are handled.
stype: The sum type object created bysum().
Returns: A function that takes a cases object.
This function, in turn, returns another function that takes an instance of the sum type and applies the matching case.
cases: An object where:- Keys are the names of the variant constructors of the
stype. - Values are functions that will be executed if the instance matches that variant. The function receives the instance (destructured or whole) as an argument.
- A special key
_(underscore) can be used as a wildcard or default case. If a specific variant is not listed incasesand no wildcard is provided,matchwill throw an error during its setup phase to enforce exhaustive matching. If a wildcard is provided, it must be a function.
- Keys are the names of the variant constructors of the
import { sum, match } from "styp";
const Option = sum("Option", {
Some: ["value"],
None: []
}, true); // structural = true
const describeOption = match(Option)({
Some: ({ value }) => `It's Some containing: ${value}`,
None: () => "It's None"
});
const option1 = Option.Some(42);
const option2 = Option.None;
console.log(describeOption(option1)); // -> "It's Some containing: 42"
console.log(describeOption(option2)); // -> "It's None"
// THIS WOULD THROW!
// console.log(describeOption({ value: 42 }))
// Example with wildcard
const handleResult = match(Result)({ // Assuming 'Result' sum type from previous example
Ok: ({ data }) => `Success: ${data}`,
_: (errInstance) => `An error occurred: ${errInstance.message || 'Unknown error'}` // Handles Err
});
console.log(handleResult(Result.Ok("Data loaded")));
console.log(handleResult(Result.Err("Network timeout")));⚠️ Note:
matchis currently a thin wrapper around thecatamethod, offering a curried pointfree way to structure functions around case analysis. It serves as a placeholder for more advanced pattern matching capabilities planned for future versions of Styp. As such, its API and behavior are subject to significant changes in future releases.matchwith structural types requires "true" instances created by Styp (i.e., objects that have internal Styp symbols like[$tag]). While a plain object might pass aninstanceofcheck for a structural type (e.g.,plainObj instanceof MyStructuralVariant), passingplainObjdirectly to the matcher function will result in an error. Always convert such plain objects to true Styp instances using.from()on the sum type or variant constructor before matching. e.g.,MySumType.from(plainObj)orMyVariant.from(plainObj).
Styp allows you to control the behavior of the instanceof operator for types created with tagged and sum using the structural boolean flag. This is achieved by customizing Symbol.hasInstance.
This is the standard JavaScript way of checking types.
instance instanceof TaggedConstructor: True ifinstancewas created byTaggedConstructor(prototype chain check).instance instanceof SingletonType: True only ifinstanceis the exact singleton object (===).instance instanceof SumType: True ifinstanceis a "true" instance of one of its variants and carries an internal Styp symbol ([$sumT]) identifying it as part of the sum type.
Type compatibility is determined by the object's "shape" (presence of fields) rather than its specific constructor or prototype chain.
obj instanceof TaggedConstructor: True ifobjis a non-null object and possesses all the fields defined forTaggedConstructoras its own properties. It does not check property types or disallow extra properties.obj instanceof SingletonType: True ifobjis any non-null object. This check is very broad. For example, ifEmpty = tagged("Empty", [], true), then{} instanceof Emptywill betrue, and indeed, any objectobjwill result inobj instanceof Emptybeingtrue.obj instanceof SumType: True ifobj instanceof VariantConstructoris true for any of theSumType's variants.
Returns a string representation of the type or constructor.
- For Tagged Constructors/Singletons:
Point.toString()->"Point",Nil.toString()->"Nil" - For Sum Types:
Result.toString()->"Result" - For Variant Constructors:
Result.Ok.toString()->"Result.Ok"
Checks if the given obj is an instance of this specific type/constructor or, for sum types, an instance of any of its variants.
- For Tagged Constructors:
Point.is(p1) - For Singletons:
Nil.is(Nil) - For Sum Types:
Result.is(success)(true ifsuccessisResult.OkorResult.Err) - For Variant Constructors:
Result.Ok.is(success)
import { tagged, sum } from "styp";
const Point = tagged("Point", ["x","y"]);
const p1 = Point(2, 7);
console.log(Point.is(p1)); // -> true
console.log(Point.is({ x:2, y:7 })); // -> false (not an instance)
const Maybe = sum("Maybe", { Just: ["v"], Nothing: [] });
const mJust = Maybe.Just(10);
console.log(Maybe.is(mJust)); // -> true
console.log(Maybe.Just.is(mJust)); // -> true
console.log(Maybe.Nothing.is(mJust)); // -> false-
For Tagged Constructors (e.g.,
Point.from(obj)):- Creates an instance of the tagged type from a plain object
obj. objmust contain properties matching the field names defined for the tagged type. Extra properties inobjare ignored.- The
typefieldparameter is not used bytagged.from().
const Point = tagged("Point", ["x","y"]); let pFromObj = Point.from({ x:10, y:3, extra:"ignored" }); console.log(pFromObj.toString()); // -> Point(10,3)
- Creates an instance of the tagged type from a plain object
-
For Sum Types (e.g.,
Maybe.from(obj, typefield?)):- Creates an instance of one of the sum type's variants from a plain object
obj. objmust have a property (whose key is specified bytypefield, defaulting to"$type") that indicates which variant constructor to use. The value of this property must be the name of a variant (e.g., "Just", "Nothing").- Other properties of
objare used as fields for that variant.
const Maybe = sum("Maybe", { Just: ["value"], Nothing: [] }); let justInstance = Maybe.from({ $type: "Just", value: 100 }); console.log(justInstance.toString()); // -> Maybe.Just(100) let nothingInstance = Maybe.from({ $type: "Nothing" }); console.log(nothingInstance.toString()); // -> Maybe.Nothing // Using a custom typefield let justInstanceCustom = Maybe.from({ kind: "Just", value: 200 }, "kind"); console.log(justInstanceCustom.toString()); // -> Maybe.Just(200)
- Creates an instance of one of the sum type's variants from a plain object
All instances created by styp constructors are immutable (Object.freeze() is applied).
Returns a string representation of the instance, including its type and field values.
const Point = tagged("Point", ["x","y"]);
console.log(Point(5,5).toString()); // -> Point(5,5)
const Maybe = sum("Maybe", { Just: ["val"], Nothing: [] });
console.log(Maybe.Just("hello").toString()); // -> Maybe.Just(hello)
console.log(Maybe.Nothing.toString()); // -> Maybe.Nothing (for singleton variants)Returns a new, plain JavaScript object representation of the instance. This is useful for serialization or interop with code that expects plain objects.
-
typefield: An optional string specifying the property name in the output object that will hold the type/variant name. Defaults to"$type". -
For instances of
taggedtypes: Thetypefieldproperty in the result will be thetypename(e.g., "Point"). -
For instances of
sumtype variants: Thetypefieldproperty in the result will be the variant's constructor name / tag (e.g., "Just", "Err").
const Point = tagged("Point", ["x", "y"]);
const p = Point(10, 20);
console.log(p.unwrap()); // -> { $type: "Point", x: 10, y: 20 }
console.log(p.unwrap("kind")); // -> { kind: "Point", x: 10, y: 20 }
const Option = sum("Option", { Some: ["value"], None: [] });
const someVal = Option.Some(42);
const noVal = Option.None;
console.log(someVal.unwrap()); // -> { $type: "Some", value: 42 }
console.log(noVal.unwrap()); // -> { $type: "None" }(Available only on instances of variants from a sum type).
Performs case analysis (matching based on the variant type) on the instance. cata is short for catamorphism. It allows you to execute different code paths depending on the specific variant of the sum type instance.
cases: An object where:- Keys are the names of the variant constructors (e.g., "Just", "Nothing").
- Values are functions that will be executed if the instance matches that variant. The function receives the instance (you can destructure its fields) as an argument.
- A special key
_(underscore) can be used as a wildcard or default case if not all variants are explicitly handled. - If the instance's variant is not found in
casesand no_wildcard is provided,catawill throw an error.
const Result = sum("Result", { Ok: ["data"], Err: ["error"] });
let success = Result.Ok("Data processed!");
let appError = Result.Err("Failed to load resource");
function handleResult(res) {
return res.cata({
Ok: ({ data }) => `Success: ${data}`,
Err: ({ error }) => `Failure: ${error}`
});
}
console.log(handleResult(success)); // -> Success: Data processed!
console.log(handleResult(appError)); // -> Failure: Failed to load resource
// With wildcard
function getMessageOrDefault(res) {
return res.cata({
Ok: ({ data }) => data,
_: () => "No specific data found." // Handles Err or any other variant
});
}
console.log(getMessageOrDefault(success)); // -> "Data processed!"
console.log(getMessageOrDefault(appError)); // -> "No specific data found."You can add methods to the prototype of constructor functions (from tagged or variants within sum) to provide shared behavior for all instances of that type.
import { tagged, sum } from "styp";
const Point = tagged("Point", ["x", "y"]);
Point.prototype.scale = function(n) {
return Point(this.x * n, this.y * n); // Create a new instance
}
console.log(Point(5, 5).scale(2).toString()); // -> Point(10,10)
const Option = sum("Option", { Some: ["x"], None: [] });
// Add map to the Option sum type's prototype
Option.prototype.map = function(fn) {
return this.cata({
Some: ({ x }) => Option.Some(fn(x)),
None: () => Option.None // or `this` if you prefer
});
};
let anOption = Option.Some(5);
console.log(anOption.map(v => v * 2).toString()); // -> Option.Some(10)
console.log(Option.None.map(v => v * 2).toString()); // -> Option.None