@clipboard-health/contract-core

中文文档
4.6.15发布于 2026/11/05MITTypeScriptESM + CJS近 30 天下载 12.4万

Shared Zod schemas for Clipboard's contracts.

原始使用文档
文档目录5 节
  1. Table of contents
  2. Install
  3. Usage
  4. Zod schemas
  5. Local development commands

@clipboard-health/contract-core

Shared Zod schemas for Clipboard's contracts.

Table of contents

Install

npm install @clipboard-health/contract-core

Usage

Zod schemas

Comma-separated array schema

commaSeparatedArray(itemSchema) validates comma-separated string or array inputs and normalizes them to typed arrays. Designed for shared contracts where the server receives comma-separated query strings and the client passes typed arrays.

// z.input  → string | string[]
// z.output → string[]
commaSeparatedArray(nonEmptyString).optional();

// z.input  → string | ("CNA" | "RN" | "LVN")[]
// z.output → ("CNA" | "RN" | "LVN")[]
commaSeparatedArray(requiredEnum(["CNA", "RN", "LVN"]));

// z.input  → string | string[]
// z.output → string[]  (each validated as ObjectId)
commaSeparatedArray(objectId);

// z.input  → string | (string | Date)[]
// z.output → Date[]
commaSeparatedArray(dateTimeSchema());

Composes with all contract-core schemas and enum helpers. Replaces z.preprocess(splitString, z.array(...)) with proper z.input typing (the splitString pattern erases input types to unknown).

DateTime schema

dateTimeSchema() validates strict ISO-8601 datetime strings and transforms them to Date objects. Unlike z.coerce.date(), it rejects loose inputs like epoch numbers and date-only strings. Composable with .optional(), .nullable(), etc. at the call site.

Discriminated union with fallback

discriminatedUnionWithFallback(discriminator, variants) takes the same arguments as z.discriminatedUnion and returns a response schema for a union whose variant set grows over time, so reads tolerate variants this consumer does not know yet.

It is for responses only. Requests declare their own z.discriminatedUnion over the same variants, where an unknown variant is a client error rather than something to tolerate.

z.discriminatedUnion (requests)discriminatedUnionWithFallback (responses)
Unknown discriminatorrejectcollapse to fallback
Unknown top-level key on a known variantstripstrip
Missing field, invalid valuerejectreject

z.union cannot express this. Its fallback branch matches any unknown value, so a known variant with a bad field fails every branch and reports an opaque invalid_union carrying one nested error per branch. Rewriting the unknown discriminator first means the response discriminates normally and reports the issue on the offending field alone, which is what makes a rejected response diagnosable from logs.

Enum validation helpers

This package provides four enum validation helpers to cover different use cases:

Fallback validation (with coalescing):

  • requiredEnumWithFallback(values) - Invalid values are coerced to ENUM_FALLBACK ("UNRECOGNIZED_"), a business-context-neutral sentinel automatically appended to the enum type. undefined fails validation.
  • optionalEnumWithFallback(values) - Invalid values are coerced to ENUM_FALLBACK ("UNRECOGNIZED_"). undefined passes through as undefined.

Strict validation (no fallback):

  • requiredEnum(values) - Wraps z.enum() for required strict validation. Invalid values fail validation.
  • optionalEnum(values) - Wraps z.enum() for optional strict validation. Invalid values fail validation, but undefined is allowed.

Type narrowing: All helpers reject widened string[] arrays at compile time. When passing a pre-declared variable, use as const to preserve literal types:

// Inline arrays work as-is
requiredEnum(["a", "b"]);

// Pre-declared variables require `as const`
const VALUES = ["a", "b"] as const;
requiredEnum(VALUES);

// Without `as const`, the type widens to string[] and is rejected
const widened = ["a", "b"];
requiredEnum(widened); // TS error
import {
  apiErrors,
  booleanString,
  commaSeparatedArray,
  dateTimeSchema,
  discriminatedUnionWithFallback,
  ENUM_FALLBACK,
  nonEmptyString,
  objectId,
  optionalEnum,
  optionalEnumWithFallback,
  requiredEnum,
  requiredEnumWithFallback,
  uuid,
} from "@clipboard-health/contract-core";
import { z, type ZodError } from "zod";

function logError(error: unknown) {
  console.error((error as ZodError).issues[0]!.message);
}

apiErrors.parse({
  errors: [
    {
      code: "NotFound",
      detail: "Resource 'b146a790-9ed1-499f-966d-6c4905dc667f' not found",
      id: "6191a8a0-96ff-4d4b-8e0f-746a5ab215f9",
      status: "404",
      title: "Not Found",
    },
  ],
});

// Comma-separated array examples
// Designed for shared contracts: server receives "CNA,RN" (string), client passes ["CNA", "RN"] (array).
// Both normalize to the same typed array output.
const workerTypes = commaSeparatedArray(requiredEnum(["CNA", "RN", "LVN"]));

// Server-side: comma-separated string from query params
const fromString = workerTypes.parse("CNA,RN");
// => ["CNA", "RN"]
console.log(fromString);

// Client-side: typed array
const fromArray = workerTypes.parse(["CNA", "LVN"]);
// => ["CNA", "LVN"]
console.log(fromArray);

// Composes with other schemas
const workerIds = commaSeparatedArray(objectId).optional();
const dates = commaSeparatedArray(dateTimeSchema());

// Works with .optional()
const noIds = workerIds.parse(undefined);
// => undefined
console.log(noIds);

const someDates = dates.parse("2026-01-01T00:00:00.000Z,2026-01-02T00:00:00.000Z");
// => [Date, Date]
console.log(someDates[0] instanceof Date); // true

booleanString.parse("true");

try {
  booleanString.parse("invalid");
} catch (error) {
  logError(error);
  // => Invalid enum value. Expected 'true' | 'false', received 'invalid'
}

nonEmptyString.parse("hello");
try {
  nonEmptyString.parse("");
} catch (error) {
  logError(error);
  // => String must contain at least 1 character(s)
}

// UUID validation examples
uuid.parse("b8d617bb-edef-4262-a6e3-6cc807fa1b26");
try {
  uuid.parse("invalid");
} catch (error) {
  logError(error);
  // => Invalid UUID format
}

// DateTime schema examples
// Accepts ISO-8601 datetime strings and Date objects, normalizes to Date.
// Composable with .optional(), .nullable(), etc.
const createdAt = dateTimeSchema().parse("2026-03-15T10:30:00.000Z");
// => Date object
console.log(createdAt instanceof Date); // true
console.log(createdAt.toISOString()); // "2026-03-15T10:30:00.000Z"

// Date objects pass through as-is
const fromDate = dateTimeSchema().parse(new Date("2026-03-15T10:30:00.000Z"));
// => Date object
console.log(fromDate instanceof Date); // true

try {
  dateTimeSchema().parse("2026-03-15"); // date-only string
} catch (error) {
  logError(error);
  // => Invalid datetime
}

try {
  dateTimeSchema().parse(1_773_340_050_000); // epoch number
} catch (error) {
  logError(error);
  // => Invalid union
}

// Optional usage — compose at the call site
const schema = dateTimeSchema().optional();
const noDate = schema.parse(undefined);
// => undefined
console.log(noDate);

const someDate = schema.parse("2026-03-15T10:30:00.000Z");
// => Date object
console.log(someDate);

// Enum with fallback examples
// ENUM_FALLBACK is a neutral sentinel ("UNRECOGNIZED_") automatically appended
// to the enum type. Consumers cannot choose their own fallback value, preventing
// misuse where a business-meaningful value is treated as a default.

/* -- required -- */
const requiredStatusEnumSchema = requiredEnumWithFallback(["pending", "completed", "failed"]);
// type RequiredStatusEnum = "pending" | "completed" | "failed" | "UNRECOGNIZED_"
type RequiredStatusEnum = z.infer<typeof requiredStatusEnumSchema>;

const completedStatus: RequiredStatusEnum = requiredStatusEnumSchema.parse("completed");
// => "completed"
console.log(completedStatus);

const additionalStatus = requiredStatusEnumSchema.parse("additional");
// => "UNRECOGNIZED_" (ENUM_FALLBACK)
console.log(additionalStatus);
console.log(additionalStatus === ENUM_FALLBACK); // true

try {
  requiredStatusEnumSchema.parse(undefined);
} catch (error) {
  logError(error);
  // => Validation error
}

/* -- optional -- */
const optionalStatusEnumSchema = optionalEnumWithFallback(["pending", "completed", "failed"]);
// type OptionalStatusEnum = "pending" | "completed" | "failed" | "UNRECOGNIZED_" | undefined
type OptionalStatusEnum = z.infer<typeof optionalStatusEnumSchema>;

const failedStatus: OptionalStatusEnum = optionalStatusEnumSchema.parse("failed");
// => "failed"
console.log(failedStatus);

const extraStatus = optionalStatusEnumSchema.parse("extra");
// => "UNRECOGNIZED_" (ENUM_FALLBACK)
console.log(extraStatus);

const undefinedStatus = optionalStatusEnumSchema.parse(undefined);
// => undefined
console.log(undefinedStatus);

// Strict enum examples (no fallback behavior)
// Use these when invalid values should fail validation rather than being coerced.
// Ideal for user types, authentication, and critical business logic.

/* -- required strict -- */
const requiredUserTypeSchema = requiredEnum(["admin", "worker", "workplace"]);
// type RequiredUserType = "admin" | "worker" | "workplace"
type RequiredUserType = z.infer<typeof requiredUserTypeSchema>;

const adminUser: RequiredUserType = requiredUserTypeSchema.parse("admin");
// => "admin"
console.log(adminUser);

try {
  requiredUserTypeSchema.parse("invalid");
} catch (error) {
  logError(error);
  // => Invalid enum value. Expected 'admin' | 'worker' | 'workplace', received 'invalid'
}

try {
  requiredUserTypeSchema.parse(undefined);
} catch (error) {
  logError(error);
  // => Required
}

/* -- optional strict -- */
const optionalUserTypeSchema = optionalEnum(["admin", "worker", "workplace"]);
// type OptionalUserType = "admin" | "worker" | "workplace" | undefined
type OptionalUserType = z.infer<typeof optionalUserTypeSchema>;

const workerUser: OptionalUserType = optionalUserTypeSchema.parse("worker");
// => "worker"
console.log(workerUser);

const noUserType = optionalUserTypeSchema.parse(undefined);
// => undefined
console.log(noUserType);

try {
  optionalUserTypeSchema.parse("invalid");
} catch (error) {
  logError(error);
  // => Invalid enum value. Expected 'admin' | 'worker' | 'workplace', received 'invalid'
}

// Discriminated union with fallback examples
// Takes the same arguments as z.discriminatedUnion and returns a response schema
// that tolerates variants this consumer does not know yet.
const notificationVariants = [
  z.object({
    channel: z.literal("EMAIL"),
    emailAddress: z.string().email(),
  }),
  z.object({
    channel: z.literal("SMS"),
    phoneNumber: nonEmptyString,
  }),
] as const;

const notificationResponseSchema = discriminatedUnionWithFallback("channel", notificationVariants);

const emailNotification = notificationResponseSchema.parse({
  channel: "EMAIL",
  emailAddress: "[email protected]",
});
// => { channel: "EMAIL", emailAddress: "[email protected]" }
console.log(emailNotification);

// A variant this consumer does not recognize yet collapses to ENUM_FALLBACK.
const unrecognizedNotification = notificationResponseSchema.parse({
  channel: "PUSH",
  deviceToken: "abc123",
});
// => { channel: "UNRECOGNIZED_" }
console.log(unrecognizedNotification);

// A known variant with a bad field still fails, naming the field.
try {
  notificationResponseSchema.parse({ channel: "EMAIL", emailAddress: 3 });
} catch (error) {
  logError(error);
  // => Expected string, received number
}

// Requests use z.discriminatedUnion directly: an unknown variant is a client
// error, not something to tolerate.
const notificationRequestSchema = z.discriminatedUnion("channel", [...notificationVariants]);

try {
  notificationRequestSchema.parse({ channel: "PUSH", deviceToken: "abc123" });
} catch (error) {
  logError(error);
  // => Invalid discriminator value. Expected 'EMAIL' | 'SMS'
}

Local development commands

See package.json scripts for a list of commands.

版本与发布记录

@clipboard-health/contract-core 共发布 490 个版本,首次发布于 2024/10/23;过去 12 个月发布了 391 个版本。

版本发布日期距今
4.6.15latest2026/11/054 天前
4.6.142026/11/054 天前
4.6.132026/11/045 天前
4.6.122026/10/3110 天前
4.6.112026/10/2812 天前
4.6.102026/10/2813 天前

关于 @clipboard-health/contract-core 的常见问题

@clipboard-health/contract-core 是什么?

Shared Zod schemas for Clipboard's contracts.

如何安装 @clipboard-health/contract-core?

在项目目录运行 npm install @clipboard-health/contract-core,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:zod(^3)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@clipboard-health/contract-core 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@clipboard-health/contract-core 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@clipboard-health/contract-core 可以商用吗?

@clipboard-health/contract-core 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@clipboard-health/contract-core 的下载量有多少?

根据 npm 官方下载统计,@clipboard-health/contract-core 在 2026/10/10 – 2026/11/08 这 30 天内被下载 124,066 次,日均约 4,136 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@clipboard-health/contract-core 还在维护吗?

最新版本 4.6.15 发布于 2026/11/05(4 天前);至今共发布 490 个版本,其中过去 12 个月发布了 391 个;首次发布于 2024/10/23。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。