@clippboard-health/util-ts (英语)
TypeScript 服务.
目录
安装
npm install @clipboard-health/util-ts
使用量
见 ./src/lib 每个工具.
服务错误
import { deepEqual, strictEqual } from "node:assert/strict";
import { ERROR_CODES, ServiceError } from "@clipboard-health/util-ts";
import { z } from "zod";
{
const error = new ServiceError("boom");
strictEqual(error.toString(), `ServiceError[${error.id}]: [internal]: boom`);
}
try {
throw new Error("boom");
} catch (error) {
const serviceError = ServiceError.fromUnknown(error);
strictEqual(serviceError.toString(), `ServiceError[${serviceError.id}]: [internal]: boom`);
}
{
const serviceError = ServiceError.fromZodError(
new z.ZodError([{ code: "custom", path: ["foo"], message: "boom" }]),
);
strictEqual(serviceError.toString(), `ServiceError[${serviceError.id}]: [badRequest]: boom`);
}
{
const errorWithCause = new ServiceError({
issues: [{ message: "boom" }],
cause: new Error("Original error"),
});
strictEqual(errorWithCause.toString(), `ServiceError[${errorWithCause.id}]: [internal]: boom`);
}
{
const multipleIssues = new ServiceError({
issues: [
{
code: ERROR_CODES.badRequest,
message: "Invalid email format",
path: ["data", "attributes", "email"],
},
{
code: ERROR_CODES.unprocessableEntity,
message: "Phone number too short",
path: ["data", "attributes", "phoneNumber"],
},
],
cause: new Error("Original error"),
});
strictEqual(
multipleIssues.toString(),
`ServiceError[${multipleIssues.id}]: [badRequest]: Invalid email format; [unprocessableEntity]: Phone number too short`,
);
deepEqual(multipleIssues.toJsonApi(), {
errors: [
{
id: multipleIssues.id,
status: "400",
code: "badRequest",
title: "Invalid or malformed request",
detail: "Invalid email format",
source: {
pointer: "/data/attributes/email",
},
},
{
id: multipleIssues.id,
status: "422",
code: "unprocessableEntity",
title: "Request failed validation",
detail: "Phone number too short",
source: {
pointer: "/data/attributes/phoneNumber",
},
},
],
});
}
服务成果
ServiceResult<A, E> 表示一个成功的 A 值或一个 ServiceError 子类型 E。 。 。 .
错误类型默认为 ServiceError 与现有呼叫者兼容。 传递明确
当呼叫者需要分解错误时, 操作可以返回 .
import { ok } from "node:assert/strict";
import {
ERROR_CODES,
failure,
isFailure,
isSuccess,
type ServiceResult,
success,
} from "@clipboard-health/util-ts";
function validateUser(params: { email: string; phone: string }): ServiceResult<{ id: string }> {
const { email, phone } = params;
const code = ERROR_CODES.unprocessableEntity;
if (!email.includes("@")) {
return failure({ issues: [{ code, message: "Invalid email format" }] });
}
if (phone.length !== 12) {
return failure({ issues: [{ code, message: "Invalid phone number" }] });
}
return success({ id: "user-123" });
}
ok(isFailure(validateUser({ email: "invalidEmail", phone: "invalidPhoneNumber" })));
ok(isSuccess(validateUser({ email: "[email protected]", phone: "555-555-5555" })));
matchServiceResult
matchServiceResult 映射输入的地图 ServiceResult 到一个输出,同时需要处理器
字义 _tag 在错误的结合。 添加或删除一个错误类型, 从而更新每个穷尽
匹配编译时间。 不需要全部处理的呼叫者可以继续使用 isFailure 财务报告和审定财务报表
isSuccess。 。 。 .
import { strictEqual } from "node:assert/strict";
import {
failure,
matchServiceResult,
ServiceError,
type ServiceResult,
success,
} from "@clipboard-health/util-ts";
interface User {
id: string;
name: string;
}
class InvalidUserIdError extends ServiceError {
public readonly _tag = "InvalidUserIdError" as const;
public constructor() {
super("User ID is required");
}
}
class UserNotFoundError extends ServiceError {
public readonly _tag = "UserNotFoundError" as const;
public readonly userId: string;
public constructor({ userId }: { userId: string }) {
super(`User ${userId} was not found`);
this.userId = userId;
}
}
type FindUserError = InvalidUserIdError | UserNotFoundError;
function findUser({ userId }: { userId: string }): ServiceResult<User, FindUserError> {
if (userId.length === 0) {
return failure(new InvalidUserIdError());
}
if (userId === "missing") {
return failure(new UserNotFoundError({ userId }));
}
return success({ id: userId, name: "Ada" });
}
const message = matchServiceResult(findUser({ userId: "missing" }), {
onSuccess: (user) => `Found ${user.name}`,
onError: {
InvalidUserIdError: (error) => error.message,
UserNotFoundError: (error) => `User ${error.userId} was not found`,
},
});
strictEqual(message, "User missing was not found");
tryCatchAsync
import { ok, strictEqual } from "node:assert/strict";
import { isFailure, isSuccess, ServiceError, tryCatchAsync } from "@clipboard-health/util-ts";
async function example() {
const successResult = await tryCatchAsync(
async () => {
const response = await fetch("https://jsonplaceholder.typicode.com/posts/1");
return (await response.json()) as { id: number };
},
(error) => new ServiceError(`Failed to fetch: ${String(error)}`),
);
ok(isSuccess(successResult));
strictEqual(successResult.value.id, 1);
const failureResult = await tryCatchAsync(
async () => await Promise.reject(new Error("Network error")),
(error) => new ServiceError(`Failed to fetch: ${String(error)}`),
);
ok(isFailure(failureResult));
strictEqual(failureResult.error.issues[0]?.message, "Failed to fetch: Error: Network error");
}
void example();
tryCatch
import { ok, strictEqual } from "node:assert/strict";
import { isFailure, isSuccess, parseJson, ServiceError, tryCatch } from "@clipboard-health/util-ts";
const successResult = tryCatch(
() => parseJson<{ name: string }>('{"name": "John"}'),
(error) => new ServiceError(`Parse error: ${String(error)}`),
);
ok(isSuccess(successResult));
strictEqual(successResult.value.name, "John");
const failureResult = tryCatch(
() => parseJson("invalid json"),
(error) => new ServiceError(`Parse error: ${String(error)}`),
);
ok(isFailure(failureResult));
ok(failureResult.error.issues[0]?.message?.includes("Parse error") === true);
fromSafeParseReturnType
import { ok, strictEqual } from "node:assert/strict";
import { fromSafeParseReturnType, isFailure, isSuccess } from "@clipboard-health/util-ts";
import { z } from "zod";
const schema = z.object({ name: z.string(), age: z.number() });
const validData = { name: "John", age: 30 };
const successResult = fromSafeParseReturnType(schema.safeParse(validData));
ok(isSuccess(successResult));
strictEqual(successResult.value.name, "John");
const invalidData = { name: "John", age: "thirty" };
const failureResult = fromSafeParseReturnType(schema.safeParse(invalidData));
ok(isFailure(failureResult));
ok(failureResult.error.issues.length > 0);
职能
pipe
import { strictEqual } from "node:assert/strict";
import { pipe } from "@clipboard-health/util-ts";
const result = pipe(
" hello world ",
(s) => s.trim(),
(s) => s.split(" "),
(array) => array.map((word) => word.charAt(0).toUpperCase() + word.slice(1)),
(array) => array.join(" "),
);
strictEqual(result, "Hello World");
option
import { strictEqual } from "node:assert/strict";
import { option as O, pipe } from "@clipboard-health/util-ts";
function double(n: number) {
return n * 2;
}
function inverse(n: number): O.Option<number> {
return n === 0 ? O.none : O.some(1 / n);
}
const result = pipe(
O.some(5),
O.map(double),
O.flatMap(inverse),
O.match(
() => "No result",
(n) => `Result is ${n}`,
),
);
strictEqual(result, "Result is 0.1");
either
import { strictEqual } from "node:assert/strict";
import { either as E, pipe } from "@clipboard-health/util-ts";
function double(n: number): number {
return n * 2;
}
function inverse(n: number): E.Either<string, number> {
return n === 0 ? E.left("Division by zero") : E.right(1 / n);
}
const result = pipe(
E.right(5),
E.map(double),
E.flatMap(inverse),
E.match(
(error) => `Error: ${error}`,
(r) => `Result is ${r}`,
),
);
strictEqual(result, "Result is 0.1");
地方发展指挥部
见 package.json scripts 用于命令列表.