Skip to content

safeTry should not require .safeUnwrap() #589

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/witty-pets-attend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'neverthrow': minor
---

safeTry should not require .safeUnwrap()
12,907 changes: 2,698 additions & 10,209 deletions package-lock.json

Large diffs are not rendered by default.

9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"scripts": {
"local-ci": "npm run typecheck && npm run lint && npm run test && npm run format && npm run build",
"test": "jest && npm run test-types",
"jest": "jest",
"test-types": "tsc --noEmit -p ./tests/tsconfig.tests.json",
"lint": "eslint ./src --ext .ts",
"format": "prettier --write 'src/**/*.ts?(x)' && npm run lint -- --fix",
Expand All @@ -36,21 +37,21 @@
"@babel/preset-typescript": "7.24.7",
"@changesets/changelog-github": "^0.5.0",
"@changesets/cli": "^2.27.7",
"@types/jest": "27.4.1",
"@types/jest": "29.5.12",
"@types/node": "^18.19.39",
"@typescript-eslint/eslint-plugin": "4.28.1",
"@typescript-eslint/parser": "4.28.1",
"babel-jest": "27.5.1",
"babel-jest": "29.7.0",
"eslint": "7.30.0",
"eslint-config-prettier": "7.1.0",
"eslint-plugin-prettier": "3.4.0",
"jest": "27.5.1",
"jest": "29.7.0",
"prettier": "2.2.1",
"rollup": "^4.18.0",
"rollup-plugin-dts": "^6.1.1",
"rollup-plugin-typescript2": "^0.32.1",
"testdouble": "3.20.2",
"ts-jest": "27.1.5",
"ts-jest": "29.2.5",
"ts-toolbelt": "9.6.0",
"typescript": "4.7.2"
},
Expand Down
12 changes: 12 additions & 0 deletions src/result-async.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,18 @@ export class ResultAsync<T, E> implements PromiseLike<Result<T, E>> {
): PromiseLike<A | B> {
return this._promise.then(successCallback, failureCallback)
}

async *[Symbol.asyncIterator](): AsyncGenerator<Err<never, E>, T> {
const result = await this._promise

if (result.isErr()) {
// @ts-expect-error -- This is structurally equivalent and safe
yield errAsync(result.error)
}

// @ts-expect-error -- This is structurally equivalent and safe
return result.value
}
}

export const okAsync = <T, E = never>(value: T): ResultAsync<T, E> =>
Expand Down
14 changes: 14 additions & 0 deletions src/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,11 @@ export class Ok<T, E> implements IResult<T, E> {
_unsafeUnwrapErr(config?: ErrorConfig): E {
throw createNeverThrowError('Called `_unsafeUnwrapErr` on an Ok', this, config)
}

// eslint-disable-next-line @typescript-eslint/no-this-alias, require-yield
*[Symbol.iterator](): Generator<Err<never, E>, T> {
return this.value
}
}

export class Err<T, E> implements IResult<T, E> {
Expand Down Expand Up @@ -467,6 +472,15 @@ export class Err<T, E> implements IResult<T, E> {
_unsafeUnwrapErr(_?: ErrorConfig): E {
return this.error
}

*[Symbol.iterator](): Generator<Err<never, E>, T> {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this
// @ts-expect-error -- This is structurally equivalent and safe
yield self
// @ts-expect-error -- This is structurally equivalent and safe
return self
}
}

export const fromThrowable = Result.fromThrowable
Expand Down
121 changes: 121 additions & 0 deletions tests/safe-try.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,3 +236,124 @@ describe("Tests if README's examples work", () => {
expect(result._unsafeUnwrap()).toBe(okValue + okValue)
})
})

describe("it yields and works without safeUnwrap", () => {
test("With synchronous Ok", () => {
const res: Result<string, string> = ok("ok");

const actual = safeTry(function* () {
const x = yield* res;
return ok(x);
});

expect(actual).toBeInstanceOf(Ok);
expect(actual._unsafeUnwrap()).toBe("ok");
});

test("With synchronous Err", () => {
const res: Result<number, string> = err("error");

const actual = safeTry(function* () {
const x = yield* res;
return ok(x);
});

expect(actual).toBeInstanceOf(Err);
expect(actual._unsafeUnwrapErr()).toBe("error");
});

const okValue = 3;
const errValue = "err!";

function good(): Result<number, string> {
return ok(okValue);
}
function bad(): Result<number, string> {
return err(errValue);
}
function promiseGood(): Promise<Result<number, string>> {
return Promise.resolve(ok(okValue));
}
function promiseBad(): Promise<Result<number, string>> {
return Promise.resolve(err(errValue));
}
function asyncGood(): ResultAsync<number, string> {
return okAsync(okValue);
}
function asyncBad(): ResultAsync<number, string> {
return errAsync(errValue);
}

test("mayFail2 error", () => {
function fn(): Result<number, string> {
return safeTry<number, string>(function* () {
const first = yield* good().mapErr((e) => `1st, ${e}`);
const second = yield* bad().mapErr((e) => `2nd, ${e}`);

return ok(first + second);
});
}

const result = fn();
expect(result.isErr()).toBe(true);
expect(result._unsafeUnwrapErr()).toBe(`2nd, ${errValue}`);
});

test("all ok", () => {
function myFunc(): Result<number, string> {
return safeTry<number, string>(function* () {
const first = yield* good().mapErr((e) => `1st, ${e}`);
const second = yield* good().mapErr((e) => `2nd, ${e}`);
return ok(first + second);
});
}

const result = myFunc();
expect(result.isOk()).toBe(true);
expect(result._unsafeUnwrap()).toBe(okValue + okValue);
});

test("async mayFail1 error", async () => {
function myFunc(): ResultAsync<number, string> {
return safeTry<number, string>(async function* () {
const first = yield* (await promiseBad()).mapErr((e) => `1st, ${e}`);
const second = yield* asyncGood().mapErr((e) => `2nd, ${e}`);
return ok(first + second);
});
}

const result = await myFunc();
expect(result.isErr()).toBe(true);
expect(result._unsafeUnwrapErr()).toBe(`1st, ${errValue}`);
});

test("async mayFail2 error", async () => {
function myFunc(): ResultAsync<number, string> {
return safeTry<number, string>(async function* () {
const goodResult = await promiseGood();
const value = yield* goodResult.mapErr((e) => `1st, ${e}`);
const value2 = yield* asyncBad().mapErr((e) => `2nd, ${e}`);

return okAsync(value + value2);
});
}

const result = await myFunc();
expect(result.isErr()).toBe(true);
expect(result._unsafeUnwrapErr()).toBe(`2nd, ${errValue}`);
});

test("promise async all ok", async () => {
function myFunc(): ResultAsync<number, string> {
return safeTry<number, string>(async function* () {
const first = yield* (await promiseGood()).mapErr((e) => `1st, ${e}`);
const second = yield* asyncGood().mapErr((e) => `2nd, ${e}`);
return ok(first + second);
});
}

const result = await myFunc();
expect(result.isOk()).toBe(true);
expect(result._unsafeUnwrap()).toBe(okValue + okValue);
});
})
39 changes: 20 additions & 19 deletions tests/tsconfig.tests.json
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
{
"compilerOptions": {
"target": "es2016",
"module": "ES2015",
"noImplicitAny": true,
"sourceMap": false,
"downlevelIteration": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"strictNullChecks": true,
"strictFunctionTypes": true,
"declaration": true,
"moduleResolution": "Node",
"baseUrl": "./src",
"lib": [
"dom",
"es2016",
"es2017.object"
],
"outDir": "dist",
"target": "es2016",
"module": "ES2015",
"noImplicitAny": true,
"sourceMap": false,
"downlevelIteration": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"strictNullChecks": true,
"strictFunctionTypes": true,
"declaration": true,
"moduleResolution": "Node",
"baseUrl": "./src",
"lib": [
"dom",
"es2016",
"es2017.object"
],
"outDir": "dist",
"skipLibCheck": true
},
"include": [
"./index.test.ts",
"./typecheck-tests.ts"
],
]
}