Skip to content
CodeSook
CodeSook

07. Implement Schemas


เราจะมาทำ Schema กันก่อน
เพื่อกำหนดก่อนว่าใน App ของเรา Data ที่ไหลไปไหลมาจะหน้าตาแบบไหน คือเป็นหน้าตาของ data ของเราที่ business เราต้องการใช้งาน และเราจะยึด Schema นี้เป็นหลัง ส่วนอื่นๆเช่น Repositories หรือ Controllers จะต้องทำตามสิ่งที่ Business ต้องการ
ไม่ว่าเราจะใช้ Database อะไร ORM ยี่ห้อไหน เราจะต้องแปลงข้อมูลให้อยู่ในรูปแบบที่เราอยากได้ นั่นก็คือ Schema นี้นี่แหละ

ในที่นี้จะใช้ Effect Schema ถ้าใครจะใช้ Zod หรือ Runtime validation ตัวอื่นก็ตามสะดวก

ข้อดีของ Effect Schema คือ

  • เราสามารถแปลงกลับไปกลับมาระหว่าง Schema กับ Typescript Type ธรรมดาได้
  • เราสามารถแปลง Schema ให้เป็น JsonSchema ซึ่งเอาไว้ใช้ทำ OpenAPI Doc ได้เลย
  • ทำ Type safe parsing ได้ด้วย คือมันจะต่างจาก Zod ตรงที่ Zod จะ parse data จาก unknown ให้มี object หน้าตาเหมือน Schema แต่ Effect Schema ก็ทำแบบนั้นได้เหมือนกัน และทำได้มากกว่าด้วย ตรงที่สามารถเลือกได้ว่าจะ parse จาก unknown หรือ parse จาก typescript type
  • Effect Schema จะเป็น Immutable โดย Default คือ object ที่ได้จากการ parsed แล้วจะแก้ไขไม่ได้ โดย Effect จะใช้เทคนิดการใส่ readonly เข้าไปด้านหน้า properties ทุกตัว ถ้าเราอยากให้ Schema ตัวไหนถูกแก้ไขได้ ต้องกำหนดเองผ่าน Schema.mutable()

Files

โครงสร้าง folders ของเราจะเป็นแบบนี้

schema files tree
src
├── configure/
├── controllers/
├── index.ts
├── repositories/
├── schema
├── branded.ts
├── employee.ts
├── employeeWithRelations.ts
├── general.ts
├── helpers.ts
├── index.ts
├── overtime.ts
└── overtimeWithRelations.ts
├── services/
└── types/

Branded Id for each Schemas

เรามาเริ่มทำ Id กันก่อน

ทำไมถึงต้องทำ Id แยก
เนื่องจากว่าใน Database เราเก็บ Id เป็นตัวเลขที่เป็น running number
และใน app ของเรา Id ก็เป็น Number เช่นกัน
แต่ เราใช้ตัวเลขแทน Id ในแต่ละ Row ใน Table ของเรา ซึ่งเราไม่สามารถเอา number อะไรก็ได้มาใช้เป็น Id ฉนั้นเพื่อให้ App เรา Safe มากที่สุด ก็เลยใช้ Branded type

Branded type เป็นการกำหนดให้ Primitive type ปกติ กลายเป็น type พิเศษที่ไม่สามารถเอา Type อื่นมาแทนได้ compiler จะบอกเราเลยว่าผิด type แต่ตัวมันยังทำงานแบบ Primitive type ได้เหมือนเดิมนะ เหมือนเป็น Primitive type นี่แหละ ยกตัวอย่างเช่นเราให้ id มี type เป็น number ถ้าเราไม่ใช้ branded type เราจะใส่ number อะไรมาก็ได้ compiler มันก็บอกว่า ok เป็น number ตรงกันเรายอมรับ ถ้า dev เผลอเอา age มาใส่ที่ id compiler ก็ยัง ok กับสิ่งนี้
แต่ถ้าใช้ branded type ที่เป็น number เวลาเราใส่ number ธรรมดาเข้ามา compiler มันจะไม่ยอม ซึ่งช่วยลดความผิดพลาดของเราไปได้

schema/branded.ts
schema/branded.ts
/* eslint-disable ts/no-redeclare */
import * as
import S
S
from "effect/Schema"
export const
const EmployeeId: S.brand<typeof S.Number, "EmployeeId">
EmployeeId
=
import S
S
.
class Number
export Number

@since3.10.0

Number
.
Pipeable.pipe<typeof S.Number, S.brand<typeof S.Number, "EmployeeId">>(this: typeof S.Number, ab: (_: typeof S.Number) => S.brand<typeof S.Number, "EmployeeId">): S.brand<typeof S.Number, "EmployeeId"> (+21 overloads)
pipe
(
import S
S
.
const brand: <typeof S.Number, "EmployeeId">(brand: "EmployeeId", annotations?: S.Annotations.Schema<number & Brand<"EmployeeId">, readonly []> | undefined) => (self: typeof S.Number) => S.brand<typeof S.Number, "EmployeeId">

Returns a nominal branded schema by applying a brand to a given schema.

Schema<A> + B -> Schema<A & Brand<B>>

@paramself - The input schema to be combined with the brand.

@parambrand - The brand to apply.

@example

import * as Schema from "effect/Schema"

const Int = Schema.Number.pipe(Schema.int(), Schema.brand("Int")) type Int = Schema.Schema.Type // number & Brand<"Int">

@since3.10.0

brand
("EmployeeId")).
brand<typeof Number$, "EmployeeId">.annotations(annotations: S.Annotations.Schema<number & Brand<"EmployeeId">, readonly []>): S.brand<typeof S.Number, "EmployeeId">

Merges a set of new annotations with existing ones, potentially overwriting any duplicates.

annotations
({
Annotations.Schema<A, TypeParameters extends ReadonlyArray<any> = readonly []>.jsonSchema?: object
jsonSchema
: {
type: string
type
: "number" } })
export type
type EmployeeId = number & Brand<"EmployeeId">
EmployeeId
=
import S
S
.
namespace Schema

@since3.10.0

@since3.10.0

Schema
.
type Schema<in out A, in out I = A, out R = never>.Type<S> = S extends S.Schema.Variance<infer A, infer _I, infer _R> ? A : never

@since3.10.0

Type
<typeof
const EmployeeId: S.brand<typeof S.Number, "EmployeeId">
EmployeeId
>
export const
const EmployeeIdFromString: S.transform<typeof S.NumberFromString, S.brand<typeof S.Number, "EmployeeId">>
EmployeeIdFromString
=
import S
S
.
const transform: <S.brand<typeof S.Number, "EmployeeId">, typeof S.NumberFromString>(from: typeof S.NumberFromString, to: S.brand<typeof S.Number, "EmployeeId">, options: {
readonly decode: (fromA: number, fromI: string) => number;
readonly encode: (toI: number, toA: number & Brand<...>) => number;
readonly strict?: true;
} | {
...;
}) => S.transform<...> (+1 overload)

Create a new Schema by transforming the input and output of an existing Schema using the provided mapping functions.

@since3.10.0

transform
(
import S
S
.
class NumberFromString

This schema transforms a string into a number by parsing the string using the parse function of the effect/Number module.

It returns an error if the value can't be converted (for example when non-numeric characters are provided).

The following special string values are supported: "NaN", "Infinity", "-Infinity".

@since3.10.0

NumberFromString
,
const EmployeeId: S.brand<typeof S.Number, "EmployeeId">
EmployeeId
,
{
decode: (id: number) => number & Brand<"EmployeeId">
decode
:
id: number
id
=>
const EmployeeId: S.brand<typeof S.Number, "EmployeeId">
EmployeeId
.
BrandSchema<number & Brand<"EmployeeId">, number, never>.make(a: number, options?: MakeOptions): number & Brand<"EmployeeId">
make
(
id: number
id
),
encode: (id: number) => number
encode
:
id: number
id
=>
id: number
id
,
},
)
export const
const OvertimeId: S.brand<typeof S.Number, "OvertimeId">
OvertimeId
=
import S
S
.
class Number
export Number

@since3.10.0

Number
.
Pipeable.pipe<typeof S.Number, S.brand<typeof S.Number, "OvertimeId">>(this: typeof S.Number, ab: (_: typeof S.Number) => S.brand<typeof S.Number, "OvertimeId">): S.brand<typeof S.Number, "OvertimeId"> (+21 overloads)
pipe
(
import S
S
.
const brand: <typeof S.Number, "OvertimeId">(brand: "OvertimeId", annotations?: S.Annotations.Schema<number & Brand<"OvertimeId">, readonly []> | undefined) => (self: typeof S.Number) => S.brand<typeof S.Number, "OvertimeId">

Returns a nominal branded schema by applying a brand to a given schema.

Schema<A> + B -> Schema<A & Brand<B>>

@paramself - The input schema to be combined with the brand.

@parambrand - The brand to apply.

@example

import * as Schema from "effect/Schema"

const Int = Schema.Number.pipe(Schema.int(), Schema.brand("Int")) type Int = Schema.Schema.Type // number & Brand<"Int">

@since3.10.0

brand
("OvertimeId")).
brand<typeof Number$, "OvertimeId">.annotations(annotations: S.Annotations.Schema<number & Brand<"OvertimeId">, readonly []>): S.brand<typeof S.Number, "OvertimeId">

Merges a set of new annotations with existing ones, potentially overwriting any duplicates.

annotations
({
Annotations.Schema<A, TypeParameters extends ReadonlyArray<any> = readonly []>.jsonSchema?: object
jsonSchema
: {
type: string
type
: "number" } })
export type
type OvertimeId = number & Brand<"OvertimeId">
OvertimeId
=
import S
S
.
namespace Schema

@since3.10.0

@since3.10.0

Schema
.
type Schema<in out A, in out I = A, out R = never>.Type<S> = S extends S.Schema.Variance<infer A, infer _I, infer _R> ? A : never

@since3.10.0

Type
<typeof
const OvertimeId: S.brand<typeof S.Number, "OvertimeId">
OvertimeId
>
export const
const OvertimeIdFromString: S.transform<typeof S.NumberFromString, S.brand<typeof S.Number, "OvertimeId">>
OvertimeIdFromString
=
import S
S
.
const transform: <S.brand<typeof S.Number, "OvertimeId">, typeof S.NumberFromString>(from: typeof S.NumberFromString, to: S.brand<typeof S.Number, "OvertimeId">, options: {
readonly decode: (fromA: number, fromI: string) => number;
readonly encode: (toI: number, toA: number & Brand<...>) => number;
readonly strict?: true;
} | {
...;
}) => S.transform<...> (+1 overload)

Create a new Schema by transforming the input and output of an existing Schema using the provided mapping functions.

@since3.10.0

transform
(
import S
S
.
class NumberFromString

This schema transforms a string into a number by parsing the string using the parse function of the effect/Number module.

It returns an error if the value can't be converted (for example when non-numeric characters are provided).

The following special string values are supported: "NaN", "Infinity", "-Infinity".

@since3.10.0

NumberFromString
,
const OvertimeId: S.brand<typeof S.Number, "OvertimeId">
OvertimeId
,
{
decode: (id: number) => number & Brand<"OvertimeId">
decode
:
id: number
id
=>
const OvertimeId: S.brand<typeof S.Number, "OvertimeId">
OvertimeId
.
BrandSchema<number & Brand<"OvertimeId">, number, never>.make(a: number, options?: MakeOptions): number & Brand<"OvertimeId">
make
(
id: number
id
),
encode: (id: number) => number
encode
:
id: number
id
=>
id: number
id
,
},
)

จากตัวอย่างด้านบนเราจะสร้าง Branded type จาก Effect schema เลย เพราะเราจะใช้ Effect schema เป็นหลักอยู่แล้ว

แต่ถ้าใครไม่ได้ใช้ Effect Schema ก็ยังสามารถสร้าง Branded type จาก Effect ได้อยู่นะ ไปดูเพิ่มเติมได้ที่ Effect Docs

หรือถ้าใครไม่อยากใช้ Effect ก็สามารถสร้าง Branded type มาใช้เองได้ไปดูเพิ่มเติมได้ที่ youtube

จากตัวอย่างด้านบนจะเห็นว่าใส่ .annotations() เข้าไปด้วย เนื่องจากว่าเราจะใช้ Effect Schema ไปสร้าง OpenAPI Docs ด้วยเลย ซึ่งมันเป็นความสามารถของ Effect Schema อยู่แล้ว ที่สร้าง JsonSchema ออกมาได้จาก Effect Schema แต่เนื่องจากว่า Branded type ไม่ได้เป็น type มาตรฐานสำหรับ JsonSchema เราก็เลยต้องระบุเองว่ามันเป็น type อะไร ตรงนี้เดี๋ยวมีอธิบายเพิ่มเติมอีกสำหรับ .annotations() กับ JsonSchema

และจะเห็นว่ามี EmployeeIdFromString ด้วย
อันนี้เอาไว้แปลง id จากต้นทางที่อยู่ในรูปของ string ให้กลายเป็น number ก่อน แล้วค่อยแปลงเป็น Branded type อีกทีนึง เราก็เลยจะใช้ S.transform()
ส่วน decode กับ encode ก็คือตอนที่เราแปลงกลับไปกลับมาให้มันทำอะไรบ้างเพื่อให้ได้ type ที่เราต้องการ

General Schema that can reuse in other Schemas

schema/general.ts
schema/general.ts
import * as
import S
S
from "effect/Schema"
export const
const TimeStampSchema: S.Struct<{
createdAt: S.SchemaClass<Date, Date, never>;
deletedAt: S.NullOr<typeof S.DateFromSelf>;
updatedAt: S.SchemaClass<Date, Date, never>;
}>
TimeStampSchema
=
import S
S
.
function Struct<{
createdAt: S.SchemaClass<Date, Date, never>;
deletedAt: S.NullOr<typeof S.DateFromSelf>;
updatedAt: S.SchemaClass<Date, Date, never>;
}>(fields: {
createdAt: S.SchemaClass<...>;
deletedAt: S.NullOr<...>;
updatedAt: S.SchemaClass<...>;
}): S.Struct<...> (+1 overload)

@since3.10.0

Struct
({
createdAt: S.SchemaClass<Date, Date, never>
createdAt
:
import S
S
.
class DateFromSelf

Describes a schema that accommodates potentially invalid Date instances, such as new Date("Invalid Date"), without rejection.

@since3.10.0

DateFromSelf
.
Annotable<SchemaClass<Date, Date, never>, Date, Date, never>.annotations(annotations: S.Annotations.Schema<Date, readonly []>): S.SchemaClass<Date, Date, never>

Merges a set of new annotations with existing ones, potentially overwriting any duplicates.

annotations
({
Annotations.Schema<Date, readonly []>.jsonSchema?: object
jsonSchema
: {
example: string
example
: "2021-01-01T00:00:00.000Z",
title: string
title
: "createdAt",
type: string
type
: "string" } }),
deletedAt: S.NullOr<typeof S.DateFromSelf>
deletedAt
:
import S
S
.
class DateFromSelf

Describes a schema that accommodates potentially invalid Date instances, such as new Date("Invalid Date"), without rejection.

@since3.10.0

DateFromSelf
.
Pipeable.pipe<typeof S.DateFromSelf, S.NullOr<typeof S.DateFromSelf>>(this: typeof S.DateFromSelf, ab: (_: typeof S.DateFromSelf) => S.NullOr<typeof S.DateFromSelf>): S.NullOr<typeof S.DateFromSelf> (+21 overloads)
pipe
(
import S
S
.
const NullOr: <S extends S.Schema.All>(self: S) => S.NullOr<S>

@since3.10.0

@since3.10.0

NullOr
).
NullOr<typeof DateFromSelf>.annotations(annotations: S.Annotations.Schema<Date | null, readonly []>): S.NullOr<typeof S.DateFromSelf>

Merges a set of new annotations with existing ones, potentially overwriting any duplicates.

annotations
({
Annotations.Schema<A, TypeParameters extends ReadonlyArray<any> = readonly []>.jsonSchema?: object
jsonSchema
: {
example: string
example
: "2021-01-01T00:00:00.000Z",
title: string
title
: "deletedAt",
type: string
type
: "string" } }),
updatedAt: S.SchemaClass<Date, Date, never>
updatedAt
:
import S
S
.
class DateFromSelf

Describes a schema that accommodates potentially invalid Date instances, such as new Date("Invalid Date"), without rejection.

@since3.10.0

DateFromSelf
.
Annotable<SchemaClass<Date, Date, never>, Date, Date, never>.annotations(annotations: S.Annotations.Schema<Date, readonly []>): S.SchemaClass<Date, Date, never>

Merges a set of new annotations with existing ones, potentially overwriting any duplicates.

annotations
({
Annotations.Schema<Date, readonly []>.jsonSchema?: object
jsonSchema
: {
example: string
example
: "2021-01-01T00:00:00.000Z",
title: string
title
: "updatedAt",
type: string
type
: "string" } }),
})

จาก code ด้านบนก็จะมี

  • createdAt
  • deletedAt
  • updatedAt

จะเห็นว่าใช้ S.DateFromSelf คือเราจะแปลงจาก Date มาเป็น Date
จริงๆเราจะไม่ได้แปลงจาก Date มาเป็น Date อย่างเดียว
คือ Date object เนี่ย มันจะมี invalid date ด้วยเช่น new Date("invalid date") ซึ่งเราจะได้ Date object มา แต่มันดัน invalid
เราเอาไปใช้งานต่อไม่ได้ ตรงนี้แหละ S.DateFromSelf มันจะคอยเช็คให้เรา ถ้าเป็น invalid date มันจะ parse ไม่ผ่าน

อีกจุดนึงคือ .annotations() อีกแล้ว เหตุผลเดียวกันกับเรื่องของ Branded type เลย
และจะเห็นว่าเราใส่ example ได้ด้วย ใส่ title ได้ด้วย ทั้งหมดนี้จะได้เห็นตอนเราใช้ OpenAPI Docs มันจะไปโผล่ในนั้น

Schema for Overtime

schema/overtime.ts
schema/overtime.ts
import * as S from "effect/Schema"
import * as Branded from "./branded.js"
import * as GeneralSchema from "./general.js"
export const Schema = S.Struct({
date: S.Union(S.Date, S.DateFromSelf).annotations({
jsonSchema: {
description: "Date or ISODate",
example: "2025-01-01",
title: "Date or ISODate",
type: "string",
},
}),
employeeId: Branded.EmployeeId,
hoursWorked: S.Number,
id: Branded.OvertimeId,
reason: S.String,
...GeneralSchema.TimeStampSchema.fields,
_tag: S.Literal("Overtime").pipe(S.optional, S.withDefaults({
constructor: () => "Overtime" as const,
decoding: () => "Overtime" as const,
})),
})
export type Overtime = S.Schema.Type<typeof Schema>
export type OvertimeEncoded = S.Schema.Encoded<typeof Schema>
export const SchemaArray = S.Array(Schema)
export type OvertimeArray = S.Schema.Type<typeof SchemaArray>
export type OvertimeArrayEncoded = S.Schema.Encoded<typeof SchemaArray>
export const CreateSchema = Schema.omit("_tag", "id", "createdAt", "updatedAt", "deletedAt")
export type CreateOvertime = S.Schema.Type<typeof CreateSchema>
export const UpdateSchema = Schema.omit("_tag", "createdAt", "updatedAt", "deletedAt")
export type UpdateOvertime = S.Schema.Type<typeof UpdateSchema>

จาก code ด้านบน เราทำ Schema ของ Overtime ก่อนตามโจทย์ของเรา

มีการใส่ property _tag เข้าไปด้วย
ตรงนี้ผมมักจะติด tag ให้กับ object ด้วย เราอาจจะได้เอาไปใช้กับ pattern matching ในภายหลัง หรืออย่างน้อยๆก็มีจุดที่เอาไว้เช็คว่าเป็น object ของอะไรตอน runtime
โดยถ้าเรา parse ผ่าน ตัว Effect Schema ก็จะติด tag ให้เราเองเลย

จะเห็นว่าเราสร้าง type สำหรับ Overtime ผ่าน S.Schema.Type<> เราจะได้ Type ที่ได้จากการ parse โดยใช้ Schema นี้
ส่วนอีกอัน type OvertimeEncoded สร้างจาก S.Schema.Encoded<> คือ type ที่ได้จากการเอา Schema type ทำการ parse กลับมาเป็น typescript type แบบธรรมดา

เรามีการสร้าง Schema สำหรับ Array ของ Overtime ด้วย

และยังสร้าง Schema สำหรับการ Create Overtime ด้วย ตอนที่เราจะส่ง data ที่ได้ไปให้กับ Database ไม่ต้องการ _tag, id, createdAt, updatedAt และ deletedAt เพราะ fields เหล่านี้มันจะถูกสร้างให้อัตโนมัติจาก database
เราสามารถใช้ .omit() มาช่วย เพื่อบอกว่าให้ตัด fields เหล่านั้นออกไปซะ

แล้วก็เหมือนกันกับ Schema สำหรับการ Update Overtime ตรงนี้เราต้องการ id ก็เลยลบออกไปเท่าที่เห็น

Create Schema for Employee

schema/employee.ts
schema/employee.ts
import * as S from "effect/Schema"
import * as Branded from "./branded.js"
import * as GeneralSchema from "./general.js"
export const Role = S.Literal("Junior_Developer", "Senior_Developer", "Lead", "C_Level")
export const Department = S.Literal("IT", "Accounting", "HR", "Manager")
export const Schema = S.Struct({
department: Department,
id: Branded.EmployeeId,
name: S.String.annotations({ jsonSchema: { example: "John Doe", title: "name", type: "string" } }),
role: Role,
...GeneralSchema.TimeStampSchema.fields,
_tag: S.Literal("Employee").pipe(S.optional, S.withDefaults({
constructor: () => "Employee" as const,
decoding: () => "Employee" as const,
})),
})
export type Employee = S.Schema.Type<typeof Schema>
export type EmployeeEncoded = S.Schema.Encoded<typeof Schema>
export const SchemaArray = S.Array(Schema)
export type EmployeeArray = S.Schema.Type<typeof SchemaArray>
export type EmployeeArrayEncoded = S.Schema.Encoded<typeof SchemaArray>
export const CreateSchema = Schema.pick("name", "role", "department")
export type CreateEmployee = S.Schema.Type<typeof CreateSchema>
export type CreateEmployeeEncoded = S.Schema.Encoded<typeof CreateSchema>
export const UpdateSchema = Schema.omit("_tag", "createdAt", "updatedAt", "deletedAt")
export type UpdateEmployee = S.Schema.Type<typeof UpdateSchema>
export type UpdateEmployeeEncoded = S.Schema.Encoded<typeof UpdateSchema>

Employee with relation schema

จาก requirements ที่ได้
employee สามารถทำ overtime ได้ ฉนั้น Employee ก็จะมี relation ไปที่ Overtime เราก็จะมาสร้าง schema เพิ่มระหว่างกัน เป็น one to many

และก็มี Array ด้วยเช่นกัน

schema/employeeWithRelations.ts
schema/employeeWithRelations.ts
import * as S from "effect/Schema"
import * as EmployeeSchema from "./employee.js"
import * as OvertimeSchema from "./overtime.js"
export const Schema = S.Struct({
...EmployeeSchema.Schema.fields,
overtimes: S.Array(OvertimeSchema.Schema),
})
export type EmployeeWithRelations = S.Schema.Type<typeof Schema>
export type EmployeeWithRelationsEncoded = S.Schema.Encoded<typeof Schema>
export const SchemaArray = S.Array(Schema)
export type EmployeeWithRelationsArray = S.Schema.Type<typeof SchemaArray>
export type EmployeeWithRelationsArrayEncoded = S.Schema.Encoded<typeof SchemaArray>

Create Overtime with relation schema

ส่วนของ Overtime Schema ก็ทำเหมือนกัน ในกรณีที่เราอยากดูว่า overtime นี้เป็นของใคร ซึ่ง 1 overtime มีได้ 1 employee

schema/overtimeWithRelations.ts
schema/overtimeWithRelations.ts
import * as S from "effect/Schema"
import * as EmployeeSchema from "./employee.js"
import * as OvertimeSchema from "./overtime.js"
export const Schema = S.Struct({
...OvertimeSchema.Schema.fields,
employee: EmployeeSchema.Schema,
})
export type OvertimeWithRelations = S.Schema.Type<typeof Schema>
export type OvertimeWithRelationsEncoded = S.Schema.Encoded<typeof Schema>
export const SchemaArray = S.Array(Schema)
export type OvertimeWithRelationsArray = S.Schema.Type<typeof SchemaArray>
export type OvertimeWithRelationsArrayEncoded = S.Schema.Encoded<typeof SchemaArray>

Helper functions work with Schemas

เรามี schema หลายอันเลย แล้วเราจะใช้มัน parse กลับไปกลับมา โดยใช้

  • function S.decodeUnknownSync() เพื่อแปลงจาก object ธรรมดาให้เป็น schema
  • และกลับกับ S.encodeSync() เพื่อแปลง schema ให้เป็น object ธรรมดา

ซึ่งส่วนตัวผมคิดว่าชื่อมัน งง ก็เลยเปลี่ยนชื่อมันด้วยการเพิ่ม Helper functions แบบนี้

schema/helpers.ts
schema/helpers.ts
import S from "effect/Schema"
export const fromObjectToSchema = S.decodeUnknownSync
export const fromSchemaToObject = S.encodeSync

Exports tree shakeable files

ทีนี้เราก็ export ทุกอย่างออกไปเพื่อการใช้งานที่ง่ายมากขึ้น

schema/index.ts
schema/index.ts
export * as Branded from "./branded.js"
export * as EmployeeSchema from "./employee.js"
export * as EmployeeWithRelationsSchema from "./employeeWithRelations.js"
export * as GeneralSchema from "./general.js"
export * as Helpers from "./helpers.js"
export * as OvertimeSchema from "./overtime.js"
export * as OvertimeWithRelationsSchema from "./overtimeWithRelations.js"