withDecodingDefaultKey 的默认值有哪些坑?

给字段写 Schema.withDecodingDefaultKey(() => "member"),类型检查直接报错;改成 Effect.succeed(new Date()) 给一个 DateTimeUtcFromString 字段,还是报错。默认值看起来只是个值,规则却比想象的多。

withDecodingDefaultKey 的默认值是一个产出 Encoded 值的 Effect,发生在解码之前,并且每次解码都会重新执行。

Effect 是一份「要做什么」的描述,Schema 的 Encoded 是数据在边界上的形态,这两点合起来就解释了下面所有的坑。Schema 在边界处解码的整体思路见 Effect Schema:在系统边界做解码,一份定义多处使用,它派生出的其它能力见 Effect Schema 写一份定义,能派生出哪些东西?。

默认值能写成函数吗?

不能,它必须是 Effect,不是 thunk:

import { Effect, Schema } from "effect"

const Bad = Schema.Struct({
  // @ts-expect-error 需要 Effect,传 () => "member" 编译不过
  role: Schema.String.pipe(Schema.withDecodingDefaultKey(() => "member"))
})
void Bad

因为是 Effect,默认值可以带副作用和服务依赖。这个错误编译期就能抓到。

默认值写 Type 一侧还是 Encoded 一侧?

写 Encoded 一侧。DateTimeUtcFromString 的 Encoded 是 string,所以默认值写字符串:

const Stamp = Schema.Struct({
  at: Schema.DateTimeUtcFromString.pipe(
    Schema.withDecodingDefaultKey(Effect.succeed("1970-01-01T00:00:00.000Z"))
  )
})
console.log(Schema.decodeUnknownSync(Stamp)({}).at) // DateTime.Utc(1970-01-01T00:00:00.000Z)

const WrongSide = Schema.DateTimeUtcFromString.pipe(
  // @ts-expect-error 传 Type(Date)会被拒绝
  Schema.withDecodingDefaultKey(Effect.succeed(new Date()))
)
void WrongSide

想直接给 Type 一侧的值,换成 withDecodingDefaultTypeKey:

const TypeSide = Schema.Struct({
  at: Schema.DateTimeUtcFromString.pipe(
    Schema.withDecodingDefaultTypeKey(
      Effect.sync(() => Schema.decodeUnknownSync(Schema.DateTimeUtcFromString)("1970-01-01T00:00:00.000Z"))
    )
  )
})
console.log(Schema.decodeUnknownSync(TypeSide)({}).at)

写错一侧同样会在编译期报错。

默认值是只算一次,还是每次解码都重算?

每次解码都会重新执行这个 Effect:

let n = 0
const Counter = Schema.Struct({
  k: Schema.Number.pipe(Schema.withDecodingDefaultKey(Effect.sync(() => ++n)))
})
const decode = Schema.decodeUnknownSync(Counter)
console.log(decode({}), decode({}), decode({ k: 9 })) // { k: 1 } { k: 2 } { k: 9 }

想要「每次都是新的值」(比如新 id)用 Effect.sync,想要「永远是同一个常量」用 Effect.succeed。这一条类型系统抓不到,只会在运行时表现为行为差异,写默认值时要有意识。

上游显式传了 undefined 会怎样?

Key 版本对应 optionalKey:键可以缺失,但不能是 undefined,显式的 undefined 会被拒绝:

try {
  decode({ k: undefined })
} catch (e) {
  console.log(String(e)) // SchemaError(Expected number at ["k"])
}

上游如果会显式传 undefined,用 withDecodingDefault(先 Schema.optional),它覆盖「缺失或 undefined」两种情况。

编码时默认值会保留吗?

默认保留。要把键省掉,传 encodingStrategy: "omit":

const Omit = Schema.Struct({
  k: Schema.Number.pipe(
    Schema.withDecodingDefaultKey(Effect.succeed(1), { encodingStrategy: "omit" })
  )
})
console.log(Schema.encodeSync(Omit)({ k: 1 }), Schema.encodeSync(Counter)({ k: 1 })) // {} { k: 1 }
版本是 effect 4.0.5,以上代码在 TypeScript strict 加 exactOptionalPropertyTypes 下编译并运行过,输出注释是我实际得到的结果。withDecodingDefault 覆盖「缺失或 undefined」这一点我没有单独跑过。