249 lines
6.0 KiB
Markdown
249 lines
6.0 KiB
Markdown
---
|
|
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)
|
|
|
|
```typescript
|
|
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)
|
|
|
|
```typescript
|
|
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)
|
|
|
|
```typescript
|
|
// 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:
|
|
|
|
```typescript
|
|
// 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:
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
| V1 | V2 |
|
|
| --------------------------------------------------- | -------------------------------------------- |
|
|
| `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
|
|
|
|
```typescript
|
|
// 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.match` → `matchError`, etc.
|
|
6. **Update imports**: Add `matchError`, `matchErrorPartial`, `isTaggedError`
|
|
|
|
## Example Full Migration
|
|
|
|
**Input:**
|
|
|
|
```typescript
|
|
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:**
|
|
|
|
```typescript
|
|
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}`,
|
|
});
|
|
```
|