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.
The module offers APIs tailored for comparing existing values of your data types. Alternatively, it provides mechanisms for defining constructors for your data types.
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
In this example, we use the Data.struct
function to create structured data objects and check their equality using Equal.equals
.
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
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.
Constructors
The module introduces a concept known as "Case classes", which automate various essential operations when defining data types. These operations include generating constructors, handling equality checks, and managing hashing.
Case classes can be defined in two primary ways:
- as plain objects using
case
ortagged
- as TypeScript classes using
Class
orTaggedClass
case
This helper 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 `Person`constPerson =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 `Person`constPerson =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 create a constructor for Person
using Data.case
.
The resulting instances come with built-in equality checks, making it simple to compare them using Equal.equals
.
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 ) // Output: { name: 'Mike', _tag: '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 ) // Output: { name: 'Mike', _tag: 'Person' }
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
.
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
TaggedClass
For those who prefer working with classes over plain objects, you can utilize Data.TaggedClass
as an alternative to tagged
.
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" })console .log (mike1 ) // Output: Person { name: 'Mike', _tag: 'Person' }// Checking equalityconsole .log (Equal .equals (mike1 ,mike2 )) // Output: trueconsole .log (Equal .equals (mike1 ,john )) // Output: false
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" })console .log (mike1 ) // Output: Person { name: 'Mike', _tag: 'Person' }// Checking equalityconsole .log (Equal .equals (mike1 ,mike2 )) // Output: trueconsole .log (Equal .equals (mike1 ,john )) // Output: false
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
Union of Tagged Structs
If you're looking to create a disjoint union of tagged structs, you can easily achieve this using Data.TaggedEnum
and Data.taggedEnum
.
This feature simplifies the process of defining and working with unions of plain objects.
Definition
Let's walk through an example to see how this works:
ts
import {Data ,Equal } from "effect"// Define a union type using TaggedEnumtypeRemoteData =Data .TaggedEnum <{Loading : {}Success : { readonlydata : string }Failure : { readonlyreason : string }}>// Create constructors for specific error typesconst {Loading ,Success ,Failure } =Data .taggedEnum <RemoteData >()// Create instances of errorsconststate1 =Loading ()conststate2 =Success ({data : "test" })conststate3 =Success ({data : "test" })conststate4 =Failure ({reason : "not found" })// Checking equalityconsole .log (Equal .equals (state2 ,state3 )) // Output: trueconsole .log (Equal .equals (state2 ,state4 )) // Output: falseconsole .log (state1 ) // Output: { _tag: 'Loading' }console .log (state2 ) // Output: { data: 'test', _tag: 'Success' }console .log (state4 ) // Output: { reason: 'not found', _tag: 'Failure' }
ts
import {Data ,Equal } from "effect"// Define a union type using TaggedEnumtypeRemoteData =Data .TaggedEnum <{Loading : {}Success : { readonlydata : string }Failure : { readonlyreason : string }}>// Create constructors for specific error typesconst {Loading ,Success ,Failure } =Data .taggedEnum <RemoteData >()// Create instances of errorsconststate1 =Loading ()conststate2 =Success ({data : "test" })conststate3 =Success ({data : "test" })conststate4 =Failure ({reason : "not found" })// Checking equalityconsole .log (Equal .equals (state2 ,state3 )) // Output: trueconsole .log (Equal .equals (state2 ,state4 )) // Output: falseconsole .log (state1 ) // Output: { _tag: 'Loading' }console .log (state2 ) // Output: { data: 'test', _tag: 'Success' }console .log (state4 ) // Output: { reason: 'not found', _tag: 'Failure' }
In this example:
- We define a
RemoteData
union type with three states:Loading
,Success
, andFailure
. - We use
Data.taggedEnum
to create constructors for these states. - We create instances of each state and check for equality using
Equal.equals
.
Note that it follows the convention within the Effect ecosystem of naming the tag field with "_tag"
.
Adding Generics
You can also create tagged unions with generics using TaggedEnum.WithGenerics
. This allows for more flexible and reusable type definitions.
ts
import {Data } from "effect"typeRemoteData <Success ,Failure > =Data .TaggedEnum <{Loading : {}Success : {data :Success }Failure : {reason :Failure }}>interfaceRemoteDataDefinition extendsData .TaggedEnum .WithGenerics <2> {readonlytaggedEnum :RemoteData <this["A"], this["B"]>}const {Loading ,Failure ,Success } =Data .taggedEnum <RemoteDataDefinition >()constloading =Loading ()constfailure =Failure ({reason : "not found" })constsuccess =Success ({data : 1 })
ts
import {Data } from "effect"typeRemoteData <Success ,Failure > =Data .TaggedEnum <{Loading : {}Success : {data :Success }Failure : {reason :Failure }}>interfaceRemoteDataDefinition extendsData .TaggedEnum .WithGenerics <2> {readonlytaggedEnum :RemoteData <this["A"], this["B"]>}const {Loading ,Failure ,Success } =Data .taggedEnum <RemoteDataDefinition >()constloading =Loading ()constfailure =Failure ({reason : "not found" })constsuccess =Success ({data : 1 })
$is and $match
The Data.taggedEnum
also provides $is
and $match
functions for type guards and pattern matching, respectively.
ts
import {Data } from "effect"typeRemoteData =Data .TaggedEnum <{Loading : {}Success : { readonlydata : string }Failure : { readonlyreason : string }}>const {$is ,$match ,Loading ,Success ,Failure } =Data .taggedEnum <RemoteData >()// Create a type guardconstisLoading =$is ("Loading")console .log (isLoading (Loading ())) // trueconsole .log (isLoading (Success ({data : "test" }))) // false// Create a matcherconstmatcher =$match ({Loading : () => "this is a Loading",Success : ({data }) => `this is a Success: ${data }`,Failure : ({reason }) => `this is a Failre: ${reason }`})console .log (matcher (Success ({data : "test" }))) // "this is a Success: test"
ts
import {Data } from "effect"typeRemoteData =Data .TaggedEnum <{Loading : {}Success : { readonlydata : string }Failure : { readonlyreason : string }}>const {$is ,$match ,Loading ,Success ,Failure } =Data .taggedEnum <RemoteData >()// Create a type guardconstisLoading =$is ("Loading")console .log (isLoading (Loading ())) // trueconsole .log (isLoading (Success ({data : "test" }))) // false// Create a matcherconstmatcher =$match ({Loading : () => "this is a Loading",Success : ({data }) => `this is a Success: ${data }`,Failure : ({reason }) => `this is a Failre: ${reason }`})console .log (matcher (Success ({data : "test" }))) // "this is a Success: test"
Errors
In Effect, errors play a crucial role, and defining and constructing them is made easier with two specialized constructors:
Error
TaggedError
Error
With Data.Error
, we can create an Error
with additional fields beyond the usual message
:
ts
import {Data } from "effect"classNotFound extendsData .Error <{message : string;file : string }> {}consterr = newNotFound ({message : "Cannot find this file",file : "foo.txt"})console .log (err instanceofError ) // Output: trueconsole .log (err .file ) // Output: foo.txtconsole .log (err )/*Output:Error: Cannot find this file... stack trace ...*/
ts
import {Data } from "effect"classNotFound extendsData .Error <{message : string;file : string }> {}consterr = newNotFound ({message : "Cannot find this file",file : "foo.txt"})console .log (err instanceofError ) // Output: trueconsole .log (err .file ) // Output: foo.txtconsole .log (err )/*Output:Error: Cannot find this file... stack trace ...*/
Additionally, NotFound
is "yieldable" as it is an Effect
, so there's no need to use Effect.fail
:
ts
import {Data ,Effect } from "effect"classNotFound extendsData .Error <{message : string;file : string }> {}constprogram =Effect .gen (function* () {yield* newNotFound ({message : "Cannot find this file",file : "foo.txt"})})
ts
import {Data ,Effect } from "effect"classNotFound extendsData .Error <{message : string;file : string }> {}constprogram =Effect .gen (function* () {yield* newNotFound ({message : "Cannot find this file",file : "foo.txt"})})
TaggedError
In Effect, there's a special convention to add a _tag
field to custom errors. This convention simplifies certain operations, such as error handling with APIs like Effect.catchTag
or Effect.catchTags
. Therefore, the TaggedError
API simplifies the process of creating custom errors by automatically adding this type of tag without needing to specify it every time you create a new error:
ts
import {Data ,Effect ,Console } from "effect"classNotFound extendsData .TaggedError ("NotFound")<{message : stringfile : string}> {}constprogram =Effect .gen (function* () {yield* newNotFound ({message : "Cannot find this file",file : "foo.txt"})}).pipe (Effect .catchTag ("NotFound", (err ) =>Console .error (`${err .message } (${err .file })`)))Effect .runPromise (program )// Output: Cannot find this file (foo.txt)
ts
import {Data ,Effect ,Console } from "effect"classNotFound extendsData .TaggedError ("NotFound")<{message : stringfile : string}> {}constprogram =Effect .gen (function* () {yield* newNotFound ({message : "Cannot find this file",file : "foo.txt"})}).pipe (Effect .catchTag ("NotFound", (err ) =>Console .error (`${err .message } (${err .file })`)))Effect .runPromise (program )// Output: Cannot find this file (foo.txt)
Native Cause Support
Adding a cause
property to errors created with Data.Error
or Data.TaggedError
integrates with the native cause
property of JavaScript's Error
:
ts
import {Data ,Effect } from "effect"classMyError extendsData .Error <{cause :Error }> {}constprogram =Effect .gen (function* () {yield* newMyError ({cause : newError ("Something went wrong")})})Effect .runPromise (program )/*Error: An error has occurredat ... {name: '(FiberFailure) Error',[Symbol(effect/Runtime/FiberFailure)]: Symbol(effect/Runtime/FiberFailure),[Symbol(effect/Runtime/FiberFailure/Cause)]: {_tag: 'Fail',error: MyErrorat ...[cause]: Error: Something went wrongat ...*/
ts
import {Data ,Effect } from "effect"classMyError extendsData .Error <{cause :Error }> {}constprogram =Effect .gen (function* () {yield* newMyError ({cause : newError ("Something went wrong")})})Effect .runPromise (program )/*Error: An error has occurredat ... {name: '(FiberFailure) Error',[Symbol(effect/Runtime/FiberFailure)]: Symbol(effect/Runtime/FiberFailure),[Symbol(effect/Runtime/FiberFailure/Cause)]: {_tag: 'Fail',error: MyErrorat ...[cause]: Error: Something went wrongat ...*/