Skip to main content

TypedData

EIP-712 typed structured data - enables human-readable, type-safe signatures in Ethereum.

Overview

TypedData implements EIP-712 for typed structured data signing. Instead of signing opaque hex strings, users sign structured data with clear field names and types. This improves UX and security by making signatures human-readable in wallets like MetaMask.

Type Definition

type TypedDataType<T = Record<string, unknown>> = {
  readonly types: {
    readonly EIP712Domain: readonly TypedDataField[];
    readonly [key: string]: readonly TypedDataField[];
  };
  readonly primaryType: string;
  readonly domain: DomainType;
  readonly message: T;
};

type TypedDataField = {
  readonly name: string;
  readonly type: string;
};

Usage

Define Typed Data

import * as TypedData from './primitives/TypedData/index.js';

const typedData = TypedData.from({
  types: {
    EIP712Domain: [
      { name: 'name', type: 'string' },
      { name: 'version', type: 'string' },
      { name: 'chainId', type: 'uint256' },
      { name: 'verifyingContract', type: 'address' },
    ],
    Person: [
      { name: 'name', type: 'string' },
      { name: 'wallet', type: 'address' },
    ],
    Mail: [
      { name: 'from', type: 'Person' },
      { name: 'to', type: 'Person' },
      { name: 'contents', type: 'string' },
    ],
  },
  primaryType: 'Mail',
  domain: {
    name: 'Ether Mail',
    version: '1',
    chainId: 1,
  },
  message: {
    from: {
      name: 'Alice',
      wallet: '0xaaa...',
    },
    to: {
      name: 'Bob',
      wallet: '0xbbb...',
    },
    contents: 'Hello, Bob!',
  },
});

Hash for Signing

import { keccak256 } from './crypto/keccak256/index.js';

const hash = TypedData.hash(typedData, { keccak256 });
// Sign with wallet
const signature = await wallet.signMessage(hash);

Encode Message

const encoded = TypedData.encode(typedData, { keccak256 });

Validate Structure

TypedData.validate(typedData); // throws if invalid

Type System

Atomic Types

// Integers
'uint8', 'uint16', 'uint32', 'uint64', 'uint128', 'uint256'
'int8', 'int16', 'int32', 'int64', 'int128', 'int256'

// Other
'address'  // Ethereum address
'bool'     // Boolean
'string'   // UTF-8 string
'bytes'    // Dynamic bytes

// Fixed bytes
'bytes1', 'bytes2', ..., 'bytes32'

Struct Types

Define custom types with named fields:
Person: [
  { name: 'name', type: 'string' },
  { name: 'wallet', type: 'address' },
]

Array Types

Use [] suffix for arrays:
Group: [
  { name: 'name', type: 'string' },
  { name: 'members', type: 'Person[]' },
]

Common Use Cases

ERC-20 Permit (Gasless Approval)

const permitData = TypedData.from({
  types: {
    EIP712Domain: [
      { name: 'name', type: 'string' },
      { name: 'version', type: 'string' },
      { name: 'chainId', type: 'uint256' },
      { name: 'verifyingContract', type: 'address' },
    ],
    Permit: [
      { name: 'owner', type: 'address' },
      { name: 'spender', type: 'address' },
      { name: 'value', type: 'uint256' },
      { name: 'nonce', type: 'uint256' },
      { name: 'deadline', type: 'uint256' },
    ],
  },
  primaryType: 'Permit',
  domain: {
    name: 'USD Coin',
    version: '2',
    chainId: 1,
    verifyingContract: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
  },
  message: {
    owner: '0x123...',
    spender: '0x456...',
    value: 1000000n,
    nonce: 0n,
    deadline: 1234567890n,
  },
});

const hash = TypedData.hash(permitData, { keccak256 });
const signature = await wallet.signMessage(hash);

// Submit permit transaction
await token.permit(owner, spender, value, deadline, v, r, s);

DAI Permit

const daiPermit = TypedData.from({
  types: {
    EIP712Domain: [
      { name: 'name', type: 'string' },
      { name: 'version', type: 'string' },
      { name: 'chainId', type: 'uint256' },
      { name: 'verifyingContract', type: 'address' },
    ],
    Permit: [
      { name: 'holder', type: 'address' },
      { name: 'spender', type: 'address' },
      { name: 'nonce', type: 'uint256' },
      { name: 'expiry', type: 'uint256' },
      { name: 'allowed', type: 'bool' },
    ],
  },
  primaryType: 'Permit',
  domain: {
    name: 'Dai Stablecoin',
    version: '1',
    chainId: 1,
    verifyingContract: '0x6b175474e89094c44da98b954eedeac495271d0f',
  },
  message: {
    holder: '0x123...',
    spender: '0x456...',
    nonce: 0n,
    expiry: 0n, // 0 = infinite
    allowed: true,
  },
});

Meta-Transactions

const metaTx = TypedData.from({
  types: {
    EIP712Domain: [
      { name: 'name', type: 'string' },
      { name: 'version', type: 'string' },
      { name: 'chainId', type: 'uint256' },
      { name: 'verifyingContract', type: 'address' },
    ],
    ForwardRequest: [
      { name: 'from', type: 'address' },
      { name: 'to', type: 'address' },
      { name: 'value', type: 'uint256' },
      { name: 'gas', type: 'uint256' },
      { name: 'nonce', type: 'uint256' },
      { name: 'data', type: 'bytes' },
    ],
  },
  primaryType: 'ForwardRequest',
  domain: {
    name: 'MinimalForwarder',
    version: '0.0.1',
    chainId: 1,
    verifyingContract: '0x123...',
  },
  message: {
    from: '0x123...',
    to: '0x456...',
    value: 0n,
    gas: 100000n,
    nonce: 0n,
    data: '0xabcd...',
  },
});

Voting/Governance

const vote = TypedData.from({
  types: {
    EIP712Domain: [
      { name: 'name', type: 'string' },
      { name: 'version', type: 'string' },
      { name: 'chainId', type: 'uint256' },
      { name: 'verifyingContract', type: 'address' },
    ],
    Ballot: [
      { name: 'proposalId', type: 'uint256' },
      { name: 'support', type: 'uint8' },
    ],
  },
  primaryType: 'Ballot',
  domain: {
    name: 'MyDAO',
    version: '1',
    chainId: 1,
    verifyingContract: '0x123...',
  },
  message: {
    proposalId: 42n,
    support: 1, // 0=against, 1=for, 2=abstain
  },
});

Hash Calculation

EIP-712 hash is computed as:
hash = keccak256(
  "\x19\x01" ||
  domainSeparator ||
  keccak256(encodeData(primaryType, message, types))
)
Where:
  • \x19\x01 - EIP-191 structured data prefix
  • domainSeparator - keccak256(encodeData(‘EIP712Domain’, domain))
  • encodeData() - Recursive encoding of message fields

Type Encoding

Simple Type

encodeType('Person', types)
// "Person(string name,address wallet)"

Nested Types

encodeType('Mail', types)
// "Mail(Person from,Person to,string contents)Person(string name,address wallet)"
Dependencies are sorted alphabetically after the primary type.

Security

Benefits

  1. Human-readable - Users see structured data, not hex
  2. Type-safe - Field types are validated
  3. Domain-specific - Signatures bound to contract/chain
  4. Replay-protected - Domain separator prevents replay

Validation

Always validate typed data:
try {
  TypedData.validate(typedData);
} catch (error) {
  console.error('Invalid typed data:', error);
}

MetaMask Display

MetaMask shows structured data clearly:
Sign TypedData

Domain
  name: "USD Coin"
  version: "2"
  chainId: 1
  verifyingContract: 0xa0b8...

Message
  owner: 0x123...
  spender: 0x456...
  value: 1000000
  nonce: 0
  deadline: 1234567890

Error Handling

TypedData operations throw typed errors for precise error handling:

InvalidTypedDataError

Thrown when typed data structure is invalid.
import * as TypedData from './primitives/TypedData/index.js';
import { InvalidTypedDataError } from './primitives/TypedData/errors.js';

try {
  TypedData.from({ types: undefined, primaryType: 'Mail', domain: {}, message: {} });
} catch (e) {
  if (e instanceof InvalidTypedDataError) {
    console.log(e.name);     // "InvalidTypedDataError"
    console.log(e.code);     // "INVALID_TYPED_DATA"
    console.log(e.value);    // The invalid typed data object
    console.log(e.expected); // "valid EIP-712 typed data"
  }
}

Validation Errors

The validate() function throws InvalidTypedDataError for various issues:
try {
  TypedData.validate(typedData);
} catch (e) {
  if (e instanceof InvalidTypedDataError) {
    // Common validation messages:
    // - "types must be an object"
    // - "types must include EIP712Domain"
    // - "primaryType must be a string"
    // - "primaryType 'X' not found in types"
    // - "domain must be an object"
    // - "message is required"
    // - "type 'X' not found in types"
    console.log(e.message);
  }
}

Specification

See Also