Evolution SDK
Encoding

TSchema

Type-safe schema definitions for Plutus data structures

TSchema

TSchema wraps Effect Schema with Plutus-specific encoding rules, giving you compile-time type safety and automatic CBOR serialization for smart contract data structures.

Define your schema once, get TypeScript types and CBOR encoding automatically.

Why TSchema?

Without TSchema (manual PlutusData):

  • Easy to mismatch types between TypeScript and on-chain
  • No compile-time validation
  • Manual CBOR encoding prone to errors
  • Field order mistakes cause validator failures

With TSchema:

  • Types inferred from schema automatically
  • Compiler catches type mismatches
  • Correct CBOR encoding guaranteed
  • Field order enforced by structure

Quick Start

Define a schema, extract types, create codec:

import { , ,  } from "@evolution-sdk/evolution"

// Define schema
const  = .({
  : .,
  : .
})

// Extract TypeScript type
type  = typeof .
// {
//   transaction_id: Uint8Array
//   output_index: bigint  
// }

// Create codec
const  = .()

// Use it
const :  = {
  : .("a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"),
  : 0n
}

const  = .()
const  = .()

The Core Schemas

ByteArray

For hashes, policy IDs, addresses, and binary data:

import { , ,  } from "@evolution-sdk/evolution"

const  = .

type  = typeof .
// Uint8Array

const :  = .(
  "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"
)

Integer

For amounts, quantities, timestamps, indices:

import { , ,  } from "@evolution-sdk/evolution"

const  = .

type  = typeof .
// bigint

const :  = 5000000n

Struct

For records with named fields (encoded as Plutus Constructor):

import { , ,  } from "@evolution-sdk/evolution"

const  = .({
  : .,
  : .
})

type  = typeof .
// {
//   payment_credential: Uint8Array
//   stake_credential: Uint8Array
// }

const  = .()

const :  = {
  : .("abc123def456abc123def456abc123def456abc123def456abc123de"),
  : .("def456abc123def456abc123def456abc123def456abc123def456ab")
}

const  = .()

Variant

For sum types (multiple possible constructors):

import { , ,  } from "@evolution-sdk/evolution"

const  = .({
  : {
    : .
  },
  : {
    : .
  }
})

type  = typeof .
// | { VerificationKey: { hash: Uint8Array } }
// | { Script: { hash: Uint8Array } }

const  = .()

// Verification key credential
const :  = {
  : {
    : .("abc123def456abc123def456abc123def456abc123def456abc123de")
  }
}

// Script credential
const :  = {
  : {
    : .("def456abc123def456abc123def456abc123def456abc123def456ab")
  }
}

const  = .()
const  = .()

Array

For lists of values:

import { , ,  } from "@evolution-sdk/evolution"

const  = .(.)

type  = typeof .
// ReadonlyArray<bigint>

const  = .()

const :  = [100n, 200n, 300n]
const  = .()

Map

For key-value mappings:

import { , ,  } from "@evolution-sdk/evolution"

const  = .(
  ., // AssetName
  .    // Quantity
)

type  = typeof .
// Map<Uint8Array, bigint>

const  = .()

const :  = new ([
  [.("546f6b656e"), 100n], // "Token" in hex
  [.("4e4654"), 1n]        // "NFT" in hex
])

const  = .()

UndefinedOr

For optional fields (Maybe pattern):

import { , ,  } from "@evolution-sdk/evolution"

const  = .({
  : .,
  : .(.)
})

type  = typeof .
// {
//   name: Uint8Array
//   nickname: Uint8Array | undefined
// }

const  = .()

const :  = {
  : .("416c696365"), // "Alice"
  : 
}

const :  = {
  : .("426f62"), // "Bob"
  : .("426f62627920") // "Bobby"
}

NullOr

Alternative to UndefinedOr — represents optional values as T | null:

import { ,  } from "@evolution-sdk/evolution"

const  = .({
  : .,
  : .(.)
})

type  = typeof .
// {
//   timeout: bigint
//   retryLimit: bigint | null
// }

const  = .()

const :  = {
  : 30000n,
  : null // No retry limit
}

Boolean

Maps true/false to Plutus constructors (True = Constr(1, []), False = Constr(0, [])):

import { ,  } from "@evolution-sdk/evolution"

const  = .({
  : .,
  : .
})

const  = .()

const  = .({ : true, : 100n })

PlutusData

Escape hatch for arbitrary PlutusData — use when a field can hold any Plutus data:

import { ,  } from "@evolution-sdk/evolution"

const  = .({
  : .,
  : . // Accepts any PlutusData
})

const  = .()

const  = .({
  : 1n,
  : .(0n, [.(42n)])
})

Literal

For enum-like constructors with no fields:

import { ,  } from "@evolution-sdk/evolution"

const  = .("Unit" as )

const  = .()

const  = .("Unit" as )

Tuple

For fixed-length positional data (encoded as a Plutus constructor with indexed fields):

import { ,  } from "@evolution-sdk/evolution"

const  = .([., .])

type  = typeof .
// readonly [Uint8Array, bigint]

const  = .()

const :  = [new (28), 42n]
const  = .()

TaggedStruct

Like Struct but includes an Effect _tag field for pattern matching:

import { ,  } from "@evolution-sdk/evolution"

const  = .("Claim", {})
const  = .("Update", {
  : .
})

type  = typeof .
// { readonly _tag: "Claim" }

type  = typeof .
// { readonly _tag: "Update"; readonly newValue: bigint }

Union (Direct)

Combine multiple schemas into a discriminated union — useful for complex redeemer types:

import { ,  } from "@evolution-sdk/evolution"

const  = .(
  .("Claim", {}),
  .("Cancel", {}),
  .("Update", { : . })
)

type  = typeof .

const  = .()

const :  = { : "Claim" }
const :  = { : "Update", : 500n }

Utility Functions

TSchema re-exports several Effect Schema utilities:

import { TSchema } from "@evolution-sdk/evolution"

// Type guard
TSchema.is(MySchema)(someValue)  // boolean

// Compose schemas
const Composed = TSchema.compose(SchemaA, SchemaB)

// Filter with refinement
const Positive = TSchema.filter(TSchema.Integer, (n) => n > 0n)

// Structural equality
const eq = TSchema.equivalence(MySchema)
eq(a, b)  // boolean

Creating Codecs

Use Data.withSchema() to create a codec from any schema:

import { , ,  } from "@evolution-sdk/evolution"

const  = .({
  : .,
  : .
})

const  = .()

// Codec provides:
// - toData(value) → PlutusData
// - fromData(data) → value
// - toCBORHex(value) → string
// - toCBORBytes(value) → Uint8Array
// - fromCBORHex(hex) → value
// - fromCBORBytes(bytes) → value

Real-World Examples

Payment Credential

import { , ,  } from "@evolution-sdk/evolution"

const  = .({
  : {
    : .
  },
  : {
    : .
  }
})

export type  = typeof .

export const  = .()

// Use in your app
const :  = {
  : {
    : .("abc123def456abc123def456abc123def456abc123def456abc123de")
  }
}

const  = .()

Escrow Datum

import { , ,  } from "@evolution-sdk/evolution"

const  = .({
  : .,
  : .,
  : .
})

export type  = typeof .

export const  = .()

// Create datum
const :  = {
  : .("abc123def456abc123def456abc123def456abc123def456abc123de"),
  : 1735689600000n,
  : 10000000n
}

// Encode for transaction
const  = .()

Multi-Action Redeemer

import { , ,  } from "@evolution-sdk/evolution"

const  = .({
  : {},
  : {},
  : {
    : .,
    : .
  }
})

export type  = typeof .

export const  = .()

// Different actions
const :  = { : {} }
const :  = { : {} }
const :  = {
  : {
    : .("def456abc123def456abc123def456abc123def456abc123def456ab"),
    : 1735776000000n
  }
}

const  = .()

CIP-68 Metadata Datum

For CIP-68 metadata with arbitrary PlutusData fields, you can build the schema manually or use the pre-built types from the plutus directory. See the Plutus Types documentation for ready-to-use CIP-68 schemas.

Nested Schemas

Schemas compose naturally:

import { , ,  } from "@evolution-sdk/evolution"

// Base schemas
const  = .({
  : { : . },
  : { : . }
})

const  = .({
  : { :  },
  : {
    : .,
    : .,
    : .
  }
})

// Composed schema
const  = .({
  : ,
  : .()
})

export type  = typeof .

export const  = .()

Type Extraction

Get TypeScript types from any schema:

import { , ,  } from "@evolution-sdk/evolution"

const  = .({
  : .,
  : .,
  : .(.)
})

// Extract type
type  = typeof .
// {
//   id: Uint8Array
//   amount: bigint
//   metadata: Uint8Array | undefined
// }

// Use in functions
function (: ) {
  .("Processing amount:", .)
}

Schema vs Direct PlutusData

ApproachType SafetyValidationCBOR EncodingUse When
TSchema✅ Compile-time✅ Automatic✅ Guaranteed correctProduction smart contracts
Direct Data❌ Runtime only❌ Manual⚠️ ManualPrototyping, debugging, tools

Rule of thumb: Use TSchema for all production smart contract integration.

Best Practices

Define schemas once: Create a schema for each datum/redeemer type and reuse it.

Match validator exactly: Your TSchema definitions must match your Plutus validator types exactly, including field names and order.

Use type extraction: Let TypeScript infer types from schemas—don't duplicate type definitions.

Test round-trips: Always verify encode → decode returns the original value.

Export types and codecs: Make both the type and codec available:

import { , ,  } from "@evolution-sdk/evolution"

const  = .({
  : .
})

export type  = typeof .
export const  = .()

Next Steps

On this page