Skip to content

Commit

Permalink
Merge pull request #589 from dmmulroy/dmmulroy/symbol-iterator
Browse files Browse the repository at this point in the history
safeTry should not require .safeUnwrap()
  • Loading branch information
supermacro authored Oct 23, 2024
2 parents ac52282 + da80693 commit 86832fe
Show file tree
Hide file tree
Showing 7 changed files with 2,875 additions and 10,232 deletions.
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"
],
]
}

0 comments on commit 86832fe

Please sign in to comment.