Back to list
dmmulroy

better-result-migrate-v2

by dmmulroy

Lightweight Result type for TypeScript with generator-based composition.

618🍴 13📅 Jan 23, 2026

SKILL.md


name: better-result-migrate-v2 description: Migrate better-result TaggedError from v1 (class-based) to v2 (factory-based) API

better-result-migrate

Migrate better-result TaggedError classes from v1 (class-based) to v2 (factory-based) API.

When to Use

  • Upgrading better-result from v1 to v2
  • User asks to migrate TaggedError classes
  • User mentions TaggedError v1/v2 migration

V1 API (old)

class FooError extends TaggedError {
  readonly _tag = "FooError" as const;
  constructor(readonly id: string) {
    super(`Foo: ${id}`);
  }
}

// Static methods on TaggedError
TaggedError.match(err, { ... })
TaggedError.matchPartial(err, { ... }, fallback)
TaggedError.isTaggedError(value)

V2 API (new)

class FooError extends TaggedError("FooError")<{
  id: string;
  message: string;
}>() {}

// Standalone functions
matchError(err, { ... })
matchErrorPartial(err, { ... }, fallback)
isTaggedError(value)
TaggedError.is(value)  // also available
FooError.is(value)     // class-specific check

Migration Rules

1. Simple class (no constructor logic)

// BEFORE
class FooError extends TaggedError {
  readonly _tag = "FooError" as const;
  constructor(readonly id: string) {
    super(`Foo: ${id}`);
  }
}

// AFTER
class FooError extends TaggedError("FooError")<{
  id: string;
  message: string;
}>() {}

// Usage changes:
// BEFORE: new FooError("123")
// AFTER:  new FooError({ id: "123", message: "Foo: 123" })

2. Class with computed message

Keep custom constructor to derive message:

// BEFORE
class NotFoundError extends TaggedError {
  readonly _tag = "NotFoundError" as const;
  constructor(readonly resource: string, readonly id: string) {
    super(`${resource} not found: ${id}`);
  }
}

// AFTER
class NotFoundError extends TaggedError("NotFoundError")<{
  resource: string;
  id: string;
  message: string;
}>() {
  constructor(args: { resource: string; id: string }) {
    super({ ...args, message: `${args.resource} not found: ${args.id}` });
  }
}

// Usage: new NotFoundError({ resource: "User", id: "123" })

3. Class with validation

Keep validation in custom constructor:

// BEFORE
class ValidationError extends TaggedError {
  readonly _tag = "ValidationError" as const;
  constructor(readonly field: string) {
    if (!field) throw new Error("field required");
    super(`Invalid: ${field}`);
  }
}

// AFTER
class ValidationError extends TaggedError("ValidationError")<{
  field: string;
  message: string;
}>() {
  constructor(args: { field: string }) {
    if (!args.field) throw new Error("field required");
    super({ ...args, message: `Invalid: ${args.field}` });
  }
}

4. Class with additional runtime properties

// BEFORE
class TimestampedError extends TaggedError {
  readonly _tag = "TimestampedError" as const;
  readonly timestamp = Date.now();
  constructor(readonly reason: string) {
    super(reason);
  }
}

// AFTER
class TimestampedError extends TaggedError("TimestampedError")<{
  reason: string;
  timestamp: number;
  message: string;
}>() {
  constructor(args: { reason: string }) {
    super({ ...args, message: args.reason, timestamp: Date.now() });
  }
}

5. Static method migrations

V1V2
TaggedError.match(err, handlers)matchError(err, handlers)
TaggedError.matchPartial(err, handlers, fallback)matchErrorPartial(err, handlers, fallback)
TaggedError.isTaggedError(x)isTaggedError(x) or TaggedError.is(x)

6. Import updates

// BEFORE
import { TaggedError } from "better-result";

// AFTER
import { TaggedError, matchError, matchErrorPartial, isTaggedError } from "better-result";

Workflow

  1. Find TaggedError classes: Search for extends TaggedError in the codebase
  2. Analyze each class:
    • Extract _tag value
    • Identify constructor params and their types
    • Check for constructor logic (validation, computed message, side effects)
  3. Transform class:
    • Simple: Remove constructor, add props to type parameter
    • Complex: Keep custom constructor, transform to object args
  4. Update usages: Change new FooError(a, b) to new FooError({ a, b, message })
  5. Migrate static methods: TaggedError.matchmatchError, etc.
  6. Update imports: Add matchError, matchErrorPartial, isTaggedError

Example Full Migration

Input:

import { TaggedError } from "better-result";

class NotFoundError extends TaggedError {
  readonly _tag = "NotFoundError" as const;
  constructor(readonly id: string) {
    super(`Not found: ${id}`);
  }
}

class NetworkError extends TaggedError {
  readonly _tag = "NetworkError" as const;
  constructor(readonly url: string, readonly status: number) {
    super(`Request to ${url} failed with ${status}`);
  }
}

type AppError = NotFoundError | NetworkError;

const handleError = (err: AppError) =>
  TaggedError.match(err, {
    NotFoundError: (e) => `Missing: ${e.id}`,
    NetworkError: (e) => `Failed: ${e.url}`,
  });

Output:

import { TaggedError, matchError } from "better-result";

class NotFoundError extends TaggedError("NotFoundError")<{
  id: string;
  message: string;
}>() {
  constructor(args: { id: string }) {
    super({ ...args, message: `Not found: ${args.id}` });
  }
}

class NetworkError extends TaggedError("NetworkError")<{
  url: string;
  status: number;
  message: string;
}>() {
  constructor(args: { url: string; status: number }) {
    super({ ...args, message: `Request to ${args.url} failed with ${args.status}` });
  }
}

type AppError = NotFoundError | NetworkError;

const handleError = (err: AppError) =>
  matchError(err, {
    NotFoundError: (e) => `Missing: ${e.id}`,
    NetworkError: (e) => `Failed: ${e.url}`,
  });

Score

Total Score

80/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 500以上

+10
最近の活動

1ヶ月以内に更新

+10
フォーク

10回以上フォークされている

+5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

+5

Reviews

💬

Reviews coming soon