Skip to content

Latest commit

 

History

History
164 lines (128 loc) · 4.8 KB

File metadata and controls

164 lines (128 loc) · 4.8 KB
id schema-optional-fields
title Optional and Nullable Fields
category objects
skillLevel beginner
tags
schema
optional
nullable
undefined
lessonOrder 27
rule
description
Optional and Nullable Fields using Schema.
summary Not all fields are required. Some are optional (may be missing), some are nullable (present but null), some accept both. You need to express these different optionality patterns while keeping types...

Problem

Not all fields are required. Some are optional (may be missing), some are nullable (present but null), some accept both. You need to express these different optionality patterns while keeping types accurate.

Solution

import { Schema } from "effect"

// ============================================
// OPTIONAL FIELDS (may be missing)
// ============================================

const UserProfile = Schema.Struct({
  id: Schema.String,
  name: Schema.String,
  // These fields may be omitted entirely
  bio: Schema.optional(Schema.String),
  website: Schema.optional(Schema.String),
  age: Schema.optional(Schema.Number),
})

type UserProfile = typeof UserProfile.Type
// { id: string; name: string; bio?: string; website?: string; age?: number }

const decodeProfile = Schema.decodeUnknownSync(UserProfile)

// Minimal required fields only
const minimal = decodeProfile({
  id: "user_123",
  name: "Alice",
})
console.log(`✅ ${minimal.name}, bio: ${minimal.bio ?? "not set"}`)

// With optional fields
const full = decodeProfile({
  id: "user_456",
  name: "Bob",
  bio: "Software developer",
  website: "https://bob.dev",
  age: 28,
})
console.log(`✅ ${full.name}, ${full.age} years old`)

// ============================================
// NULLABLE FIELDS (present but null)
// ============================================

const DatabaseRecord = Schema.Struct({
  id: Schema.String,
  // Field exists but value may be null
  deletedAt: Schema.NullOr(Schema.Date),
  parentId: Schema.NullOr(Schema.String),
})

type DatabaseRecord = typeof DatabaseRecord.Type
// { id: string; deletedAt: Date | null; parentId: string | null }

const decodeRecord = Schema.decodeUnknownSync(DatabaseRecord)

const activeRecord = decodeRecord({
  id: "rec_123",
  deletedAt: null,
  parentId: "rec_100",
})
console.log(`✅ Record ${activeRecord.id}, deleted: ${activeRecord.deletedAt === null}`)

const deletedRecord = decodeRecord({
  id: "rec_456",
  deletedAt: new Date(),
  parentId: null,
})
console.log(`✅ Record deleted at: ${deletedRecord.deletedAt?.toISOString()}`)

// ============================================
// OPTIONAL + NULLABLE (both)
// ============================================

const FlexibleProfile = Schema.Struct({
  id: Schema.String,
  // May be missing OR present as null
  nickname: Schema.optional(Schema.NullOr(Schema.String)),
})

type FlexibleProfile = typeof FlexibleProfile.Type
// { id: string; nickname?: string | null }

const decodeFlexible = Schema.decodeUnknownSync(FlexibleProfile)

// Field missing
const noNickname = decodeFlexible({ id: "1" })
console.log(`Nickname: ${noNickname.nickname}`)  // undefined

// Field is null
const nullNickname = decodeFlexible({ id: "2", nickname: null })
console.log(`Nickname: ${nullNickname.nickname}`)  // null

// Field has value
const hasNickname = decodeFlexible({ id: "3", nickname: "Ace" })
console.log(`Nickname: ${hasNickname.nickname}`)  // "Ace"

// ============================================
// OPTIONAL WITH DEFAULTS
// ============================================

const Settings = Schema.Struct({
  theme: Schema.optionalWith(Schema.String, { default: () => "light" }),
  pageSize: Schema.optionalWith(Schema.Number, { default: () => 20 }),
  notifications: Schema.optionalWith(Schema.Boolean, { default: () => true }),
})

type Settings = typeof Settings.Type

const decodeSettings = Schema.decodeUnknownSync(Settings)

const defaultSettings = decodeSettings({})
console.log(`✅ Theme: ${defaultSettings.theme}, Page: ${defaultSettings.pageSize}`)
// Output: ✅ Theme: light, Page: 20

const customSettings = decodeSettings({ theme: "dark", pageSize: 50 })
console.log(`✅ Theme: ${customSettings.theme}, Page: ${customSettings.pageSize}`)
// Output: ✅ Theme: dark, Page: 50

Why This Works

Pattern Type Use Case
optional(S) T | undefined Field may be omitted
NullOr(S) T | null Field present but nullable
optional(NullOr(S)) T | null | undefined Both patterns
optionalWith(S, {default}) T Default when missing

When to Use

  • User profiles with optional info
  • Database records with nullable columns
  • API responses with optional fields
  • Config with sensible defaults

Related Patterns