Data
On this page
The Data module offers a range of features that make it easier to create and manipulate data structures in your TypeScript applications. It includes functionalities for defining data types, ensuring equality between data objects, and hashing data for efficient comparison.
Value Equality
If you need to compare existing values for equality without the need for explicit
implementations, consider using the Data module. It provides convenient APIs
that generate default implementations for Equal
and Hash
, making equality
checks a breeze.
struct
ts
import {Data ,Equal } from "effect"constalice =Data .struct ({name : "Alice",age : 30 })constbob =Data .struct ({name : "Bob",age : 40 })console .log (Equal .equals (alice ,alice )) // Output: trueconsole .log (Equal .equals (alice ,Data .struct ({name : "Alice",age : 30 }))) // Output: trueconsole .log (Equal .equals (alice , {name : "Alice",age : 30 })) // Output: falseconsole .log (Equal .equals (alice ,bob )) // Output: false
ts
import {Data ,Equal } from "effect"constalice =Data .struct ({name : "Alice",age : 30 })constbob =Data .struct ({name : "Bob",age : 40 })console .log (Equal .equals (alice ,alice )) // Output: trueconsole .log (Equal .equals (alice ,Data .struct ({name : "Alice",age : 30 }))) // Output: trueconsole .log (Equal .equals (alice , {name : "Alice",age : 30 })) // Output: falseconsole .log (Equal .equals (alice ,bob )) // Output: false
In this example, we use the Data.struct
function to create structured data objects and check their equality using Equal.equals()
. The Data
module simplifies the process by providing a default implementation for both Equal
and Hash
, allowing you to focus on comparing values without the need for explicit implementations.
tuple
If you prefer to model your domain with tuples, the Data.tuple
function has got you covered:
ts
import {Data ,Equal } from "effect"constalice =Data .tuple ("Alice", 30)constbob =Data .tuple ("Bob", 40)console .log (Equal .equals (alice ,alice )) // Output: trueconsole .log (Equal .equals (alice ,Data .tuple ("Alice", 30))) // Output: trueconsole .log (Equal .equals (alice , ["Alice", 30])) // Output: falseconsole .log (Equal .equals (alice ,bob )) // Output: false
ts
import {Data ,Equal } from "effect"constalice =Data .tuple ("Alice", 30)constbob =Data .tuple ("Bob", 40)console .log (Equal .equals (alice ,alice )) // Output: trueconsole .log (Equal .equals (alice ,Data .tuple ("Alice", 30))) // Output: trueconsole .log (Equal .equals (alice , ["Alice", 30])) // Output: falseconsole .log (Equal .equals (alice ,bob )) // Output: false
array
You can take it a step further and use arrays to compare multiple values:
ts
import {Data ,Equal } from "effect"constalice =Data .struct ({name : "Alice",age : 30 })constbob =Data .struct ({name : "Bob",age : 40 })constpersons =Data .array ([alice ,bob ])console .log (Equal .equals (persons ,Data .array ([Data .struct ({name : "Alice",age : 30 }),Data .struct ({name : "Bob",age : 40 })]))) // Output: true
ts
import {Data ,Equal } from "effect"constalice =Data .struct ({name : "Alice",age : 30 })constbob =Data .struct ({name : "Bob",age : 40 })constpersons =Data .array ([alice ,bob ])console .log (Equal .equals (persons ,Data .array ([Data .struct ({name : "Alice",age : 30 }),Data .struct ({name : "Bob",age : 40 })]))) // Output: true
In this extended example, we create an array of person objects using the Data.array
function. We then compare this array with another array of person objects using Equal.equals()
, and the result is true
since the arrays contain structurally equal elements.
Case Classes
The module introduces the concept of "Case" classes. Case classes are a feature introduced by this module that automates several critical operations when creating data types. These operations include generating constructors, handling equality checks, and managing hashing.
Case classes can be defined in two main ways: as structs using Case
, case
, and tagged
, or as classes using Class
or TaggedClass
.
case
Let's start by creating a case class using Case
and case
. This combination automatically provides implementations for constructors, equality checks, and hashing for your data type.
ts
import {Data ,Equal } from "effect"interfacePerson {readonlyname : string}// Creating a constructor for the specified CaseconstPerson =Data .case <Person >()// Creating instances of Personconstmike1 =Person ({name : "Mike" })constmike2 =Person ({name : "Mike" })constjohn =Person ({name : "John" })// Checking equalityconsole .log (Equal .equals (mike1 ,mike2 )) // Output: trueconsole .log (Equal .equals (mike1 ,john )) // Output: false
ts
import {Data ,Equal } from "effect"interfacePerson {readonlyname : string}// Creating a constructor for the specified CaseconstPerson =Data .case <Person >()// Creating instances of Personconstmike1 =Person ({name : "Mike" })constmike2 =Person ({name : "Mike" })constjohn =Person ({name : "John" })// Checking equalityconsole .log (Equal .equals (mike1 ,mike2 )) // Output: trueconsole .log (Equal .equals (mike1 ,john )) // Output: false
Here, we define a Person
data type, we then create a constructor for Person
using Data.case
.
The resulting Person
instances come with built-in equality checks thanks to the Data module, making it simple to compare them using Equal.equals
.
If you prefer working with classes instead of plain objects, you can explore the use of Data.Class
.
tagged
In certain situations, like when you're defining a data type that includes a tag field (commonly used in disjoint unions), using the case
approach can become repetitive and cumbersome. This is because you're required to specify the tag every time you create an instance:
ts
import {Data } from "effect"interfacePerson {readonly_tag : "Person" // the tagreadonlyname : string}constPerson =Data .case <Person >()// It can be quite frustrating to repeat `_tag: 'Person'` every time...constmike =Person ({_tag : "Person",name : "Mike" })constjohn =Person ({_tag : "Person",name : "John" })
ts
import {Data } from "effect"interfacePerson {readonly_tag : "Person" // the tagreadonlyname : string}constPerson =Data .case <Person >()// It can be quite frustrating to repeat `_tag: 'Person'` every time...constmike =Person ({_tag : "Person",name : "Mike" })constjohn =Person ({_tag : "Person",name : "John" })
To make your life easier, the tagged
helper simplifies this process by allowing you to define the tag only once. It follows the convention within the Effect ecosystem of naming the tag field with "_tag"
:
ts
import {Data } from "effect"interfacePerson {readonly_tag : "Person" // the tagreadonlyname : string}constPerson =Data .tagged <Person >("Person")// Now, it's much more convenient...constmike =Person ({name : "Mike" })constjohn =Person ({name : "John" })console .log (mike ._tag ) // Output: "Person"
ts
import {Data } from "effect"interfacePerson {readonly_tag : "Person" // the tagreadonlyname : string}constPerson =Data .tagged <Person >("Person")// Now, it's much more convenient...constmike =Person ({name : "Mike" })constjohn =Person ({name : "John" })console .log (mike ._tag ) // Output: "Person"
This approach significantly reduces redundancy and improves code readability when working with tagged data types.
If you prefer working with classes instead of plain objects, you can explore the use of Data.TaggedClass
.
Class
If you find it more comfortable to work with classes instead of plain objects, you have the option to use Data.Class
instead of Case
and case
. This approach can be particularly useful in scenarios where you prefer a more class-oriented structure:
ts
import {Data ,Equal } from "effect"classPerson extendsData .Class <{name : string }> {}// Creating instances of Personconstmike1 = newPerson ({name : "Mike" })constmike2 = newPerson ({name : "Mike" })constjohn = newPerson ({name : "John" })// Checking equalityconsole .log (Equal .equals (mike1 ,mike2 )) // Output: trueconsole .log (Equal .equals (mike1 ,john )) // Output: false
ts
import {Data ,Equal } from "effect"classPerson extendsData .Class <{name : string }> {}// Creating instances of Personconstmike1 = newPerson ({name : "Mike" })constmike2 = newPerson ({name : "Mike" })constjohn = newPerson ({name : "John" })// Checking equalityconsole .log (Equal .equals (mike1 ,mike2 )) // Output: trueconsole .log (Equal .equals (mike1 ,john )) // Output: false
One advantage of using classes is that you can easily add custom getters and methods to the class definition, enhancing its functionality to suit your specific needs:
ts
import {Data } from "effect"classPerson extendsData .Class <{name : string }> {getupperName () {return this.name .toUpperCase ()}}constmike = newPerson ({name : "Mike" })console .log (mike .upperName ) // Output: MIKE
ts
import {Data } from "effect"classPerson extendsData .Class <{name : string }> {getupperName () {return this.name .toUpperCase ()}}constmike = newPerson ({name : "Mike" })console .log (mike .upperName ) // Output: MIKE
By incorporating custom methods like upperName
, you can extend the capabilities of your data class to perform various operations tailored to your application requirements.
TaggedClass
For those who prefer working with classes over plain objects, you can utilize Data.TaggedClass
as an alternative to Case
and tagged
. This approach can be especially beneficial when you want to structure your data using class-based syntax:
ts
import {Data ,Equal } from "effect"classPerson extendsData .TaggedClass ("Person")<{name : string }> {}// Creating instances of Personconstmike1 = newPerson ({name : "Mike" })constmike2 = newPerson ({name : "Mike" })constjohn = newPerson ({name : "John" })// Checking equalityconsole .log (Equal .equals (mike1 ,mike2 )) // Output: trueconsole .log (Equal .equals (mike1 ,john )) // Output: falseconsole .log (mike1 ._tag ) // Output: "Person"
ts
import {Data ,Equal } from "effect"classPerson extendsData .TaggedClass ("Person")<{name : string }> {}// Creating instances of Personconstmike1 = newPerson ({name : "Mike" })constmike2 = newPerson ({name : "Mike" })constjohn = newPerson ({name : "John" })// Checking equalityconsole .log (Equal .equals (mike1 ,mike2 )) // Output: trueconsole .log (Equal .equals (mike1 ,john )) // Output: falseconsole .log (mike1 ._tag ) // Output: "Person"
One of the advantages of using tagged classes is that you can seamlessly incorporate custom getters and methods into the class definition, expanding its functionality as needed:
ts
import {Data } from "effect"classPerson extendsData .TaggedClass ("Person")<{name : string }> {getupperName () {return this.name .toUpperCase ()}}constmike = newPerson ({name : "Mike" })console .log (mike .upperName ) // Output: MIKE
ts
import {Data } from "effect"classPerson extendsData .TaggedClass ("Person")<{name : string }> {getupperName () {return this.name .toUpperCase ()}}constmike = newPerson ({name : "Mike" })console .log (mike .upperName ) // Output: MIKE
By introducing custom getters such as upperName
, you can extend the capabilities of your tagged class to suit your specific application requirements.
Unions of Case Classes
If you're looking to create a disjoint union of tagged case classes, you can easily achieve this using Data.TaggedEnum
. This feature simplifies the process of defining and working with unions.
Let's walk through an example:
ts
import {Data ,Equal } from "effect"// Define a union type using TaggedEnumtypeHttpError =Data .TaggedEnum <{InternalServerError : {reason : string }NotFound : {}}>// Create constructors for specific error typesconst {NotFound ,InternalServerError } =Data .taggedEnum <HttpError >()// Create instances of errorsconsterror1 =InternalServerError ({reason : "test" })consterror2 =InternalServerError ({reason : "test" })consterror3 =NotFound ()// Checking equalityconsole .log (Equal .equals (error1 ,error2 )) // Output: trueconsole .log (Equal .equals (error1 ,error3 )) // Output: falseconsole .log (error1 ._tag ) // Output: "InternalServerError"console .log (error3 ._tag ) // Output: "NotFound"
ts
import {Data ,Equal } from "effect"// Define a union type using TaggedEnumtypeHttpError =Data .TaggedEnum <{InternalServerError : {reason : string }NotFound : {}}>// Create constructors for specific error typesconst {NotFound ,InternalServerError } =Data .taggedEnum <HttpError >()// Create instances of errorsconsterror1 =InternalServerError ({reason : "test" })consterror2 =InternalServerError ({reason : "test" })consterror3 =NotFound ()// Checking equalityconsole .log (Equal .equals (error1 ,error2 )) // Output: trueconsole .log (Equal .equals (error1 ,error3 )) // Output: falseconsole .log (error1 ._tag ) // Output: "InternalServerError"console .log (error3 ._tag ) // Output: "NotFound"
Note that it follows the convention within the Effect ecosystem of naming the tag field with "_tag"
.
You can also pass a TaggedEnum.WithGenerics
if you want to add generics to the constructors:
ts
import {Data } from "effect"typeRemoteData <A ,B > =Data .TaggedEnum <{Loading : {}Success : {data :A }Failure : {error :B }}>interfaceRemoteDataDefinition extendsData .TaggedEnum .WithGenerics <2> {readonlytaggedEnum :RemoteData <this["A"], this["B"]>}const {Loading ,Failure ,Success } =Data .taggedEnum <RemoteDataDefinition >()constloading =Loading ()constfailure =Failure ({error : "err" })constsuccess =Success ({data : 1 })
ts
import {Data } from "effect"typeRemoteData <A ,B > =Data .TaggedEnum <{Loading : {}Success : {data :A }Failure : {error :B }}>interfaceRemoteDataDefinition extendsData .TaggedEnum .WithGenerics <2> {readonlytaggedEnum :RemoteData <this["A"], this["B"]>}const {Loading ,Failure ,Success } =Data .taggedEnum <RemoteDataDefinition >()constloading =Loading ()constfailure =Failure ({error : "err" })constsuccess =Success ({data : 1 })