Skip to content
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

Moved repo to strict: true and restructured tsconfig.jsons to use project references #567

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
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
3 changes: 3 additions & 0 deletions .vscode/settings.json
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think neverthrow should include editor specific files. Please remove.

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
}
5 changes: 3 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,10 @@
"dist"
],
"scripts": {
"test": "jest && npm run test-types",
"test-types": "tsc --noEmit -p ./tests/tsconfig.tests.json",
"test": "jest",
"lint": "eslint ./src --ext .ts",
"format": "prettier --write 'src/**/*.ts?(x)' && npm run lint -- --fix",
"typecheck": "tsc --noEmit",
"typecheck": "tsc -b",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now, this typecheck command checks the entire repo, including the typecheck tests.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this create artifacts, though? Shouldn't it be tsc -b --noEmit?

The whole intention of the typecheck script is to typecheck without generating artifacts.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, noEmit has been moved to the config file - this is so that users running tsc without using the command don't accidentally generate artifacts.

"clean": "rm -rf ./dist ./tmp",
"build": "npm run clean && rollup --config && mv tmp/*.js dist",
"prepack": "npm run build",
Expand Down
3 changes: 2 additions & 1 deletion src/result-async.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ export class ResultAsync<T, E> implements PromiseLike<Result<T, E>> {
return new Err(errorFn ? errorFn(error) : error)
}
})(),
)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
) as any
}
}

Expand Down
6 changes: 4 additions & 2 deletions src/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,16 @@ export namespace Result {
fn: Fn,
errorFn?: (e: unknown) => E,
): (...args: Parameters<Fn>) => Result<ReturnType<Fn>, E> {
return (...args) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return ((...args: any[]) => {
try {
const result = fn(...args)
return ok(result)
} catch (e) {
return err(errorFn ? errorFn(e) : e)
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}) as any
Comment on lines +26 to +35
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain the rationale for these as any changes made here and in other files?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving to strict: true revealed some errors that I didn't have time to debug. I suggest we 'as any' them now, and fix them in a followup PR.

Copy link

@boyeln boyeln Sep 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason this errors and you have to as any it is because with this call signature you can in theory do something like:

const safeParse = fromThrowable<typeof JSON.parse, number>(JSON.parse);
      // ^ (text: string, reviver?: (this: any, key: string, value: any) => any) => Result<any, number>

So by allowing providing a specific generic type as E without providing an errorFn, you can get in trouble. You can then end up in a case where you try to pass e to the err in the catch block (in the second/false ternary case), but since the user might have specified a specific E (like number in the example above), you can't just give it the unknown (or any here I suppose)e.

The easiest fix might be to add some overloads to prevent using the function as I did above:

export function fromThrowable<Fn extends (...args: readonly any[]) => any>(
  fn: Fn
): (...args: Parameters<Fn>) => Result<ReturnType<Fn>, unknown>;
export function fromThrowable<Fn extends (...args: readonly any[]) => any, E>(
    fn: Fn,
    errorFn: (e: unknown) => E,
  ): (...args: Parameters<Fn>) => Result<ReturnType<Fn>, E>;
  export function fromThrowable<Fn extends (...args: readonly any[]) => any, E>(
    fn: Fn,
    errorFn?: (e: unknown) => E,
  ): (...args: Parameters<Fn>) => Result<ReturnType<Fn>, E | unknown> {
    // implementation
}

I don't know if it really makes any practical difference here, but I believe it would be more correct to do catch (e: unknown) to have e typed as unknown and not the default any.

This could of course happen in a different PR. And I suppose in theory changing the call signature as I proposed might be considered a breaking change. However, if someone is using the function as I did above they should stop doing as it probably gives you types that don't match the runtime types. So you could consider it a bug maybe 🤷

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created a separate PR for fixing the issue: #583 However, feel free to close that PR and include the changes here instead if that's better.

}

export function combine<
Expand Down
107 changes: 50 additions & 57 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,15 +333,15 @@ describe('Result.fromThrowable', () => {

// Added for issue #300 -- the test here is not so much that expectations are met as that the test compiles.
it('Accepts an inner function which takes arguments', () => {
const hello = (fname: string): string => `hello, ${fname}`;
const safeHello = Result.fromThrowable(hello);
const hello = (fname: string): string => `hello, ${fname}`
const safeHello = Result.fromThrowable(hello)

const result = hello('Dikembe');
const safeResult = safeHello('Dikembe');
const result = hello('Dikembe')
const safeResult = safeHello('Dikembe')

expect(safeResult).toBeInstanceOf(Ok);
expect(result).toEqual(safeResult._unsafeUnwrap());
});
expect(safeResult).toBeInstanceOf(Ok)
expect(result).toEqual(safeResult._unsafeUnwrap())
})

it('Creates a function that returns an err when the inner function throws', () => {
const thrower = (): string => {
Expand Down Expand Up @@ -376,7 +376,7 @@ describe('Result.fromThrowable', () => {
})

it('has a top level export', () => {
expect(fromThrowable).toBe(Result.fromThrowable)
expect(fromThrowable).toBe(Result.fromThrowable)
})
})

Expand Down Expand Up @@ -407,37 +407,34 @@ describe('Utils', () => {
})

it('Combines heterogeneous lists', () => {
type HeterogenousList = [ Result<string, string>, Result<number, number>, Result<boolean, boolean> ]

const heterogenousList: HeterogenousList = [
ok('Yooooo'),
ok(123),
ok(true),
type HeterogenousList = [
Result<string, string>,
Result<number, number>,
Result<boolean, boolean>,
]

type ExpecteResult = Result<[ string, number, boolean ], string | number | boolean>
const heterogenousList: HeterogenousList = [ok('Yooooo'), ok(123), ok(true)]

type ExpecteResult = Result<[string, number, boolean], string | number | boolean>

const result: ExpecteResult = Result.combine(heterogenousList)

expect(result._unsafeUnwrap()).toEqual(['Yooooo', 123, true])
})

it('Does not destructure / concatenate arrays', () => {
type HomogenousList = [
Result<string[], boolean>,
Result<number[], string>,
]
type HomogenousList = [Result<string[], boolean>, Result<number[], string>]

const homogenousList: HomogenousList = [
ok(['hello', 'world']),
ok([1, 2, 3])
]
const homogenousList: HomogenousList = [ok(['hello', 'world']), ok([1, 2, 3])]

type ExpectedResult = Result<[ string[], number[] ], boolean | string>
type ExpectedResult = Result<[string[], number[]], boolean | string>

const result: ExpectedResult = Result.combine(homogenousList)

expect(result._unsafeUnwrap()).toEqual([ [ 'hello', 'world' ], [ 1, 2, 3 ]])
expect(result._unsafeUnwrap()).toEqual([
['hello', 'world'],
[1, 2, 3],
])
})
})

Expand All @@ -446,7 +443,7 @@ describe('Utils', () => {
const asyncResultList = [okAsync(123), okAsync(456), okAsync(789)]

const resultAsync: ResultAsync<number[], never[]> = ResultAsync.combine(asyncResultList)

expect(resultAsync).toBeInstanceOf(ResultAsync)

const result = await ResultAsync.combine(asyncResultList)
Expand Down Expand Up @@ -481,14 +478,14 @@ describe('Utils', () => {
okAsync('Yooooo'),
okAsync(123),
okAsync(true),
okAsync([ 1, 2, 3]),
okAsync([1, 2, 3]),
]

type ExpecteResult = Result<[ string, number, boolean, number[] ], string | number | boolean>
type ExpecteResult = Result<[string, number, boolean, number[]], string | number | boolean>

const result: ExpecteResult = await ResultAsync.combine(heterogenousList)

expect(result._unsafeUnwrap()).toEqual(['Yooooo', 123, true, [ 1, 2, 3 ]])
expect(result._unsafeUnwrap()).toEqual(['Yooooo', 123, true, [1, 2, 3]])
})
})
})
Expand Down Expand Up @@ -518,37 +515,34 @@ describe('Utils', () => {
})

it('Combines heterogeneous lists', () => {
type HeterogenousList = [ Result<string, string>, Result<number, number>, Result<boolean, boolean> ]

const heterogenousList: HeterogenousList = [
ok('Yooooo'),
ok(123),
ok(true),
type HeterogenousList = [
Result<string, string>,
Result<number, number>,
Result<boolean, boolean>,
]

type ExpecteResult = Result<[ string, number, boolean ], (string | number | boolean)[]>
const heterogenousList: HeterogenousList = [ok('Yooooo'), ok(123), ok(true)]

type ExpecteResult = Result<[string, number, boolean], (string | number | boolean)[]>

const result: ExpecteResult = Result.combineWithAllErrors(heterogenousList)

expect(result._unsafeUnwrap()).toEqual(['Yooooo', 123, true])
})

it('Does not destructure / concatenate arrays', () => {
type HomogenousList = [
Result<string[], boolean>,
Result<number[], string>,
]
type HomogenousList = [Result<string[], boolean>, Result<number[], string>]

const homogenousList: HomogenousList = [
ok(['hello', 'world']),
ok([1, 2, 3])
]
const homogenousList: HomogenousList = [ok(['hello', 'world']), ok([1, 2, 3])]

type ExpectedResult = Result<[ string[], number[] ], (boolean | string)[]>
type ExpectedResult = Result<[string[], number[]], (boolean | string)[]>

const result: ExpectedResult = Result.combineWithAllErrors(homogenousList)

expect(result._unsafeUnwrap()).toEqual([ [ 'hello', 'world' ], [ 1, 2, 3 ]])
expect(result._unsafeUnwrap()).toEqual([
['hello', 'world'],
[1, 2, 3],
])
})
})
describe('`ResultAsync.combineWithAllErrors`', () => {
Expand Down Expand Up @@ -576,15 +570,15 @@ describe('Utils', () => {
})

it('Combines heterogeneous lists', async () => {
type HeterogenousList = [ ResultAsync<string, string>, ResultAsync<number, number>, ResultAsync<boolean, boolean> ]

const heterogenousList: HeterogenousList = [
okAsync('Yooooo'),
okAsync(123),
okAsync(true),
type HeterogenousList = [
ResultAsync<string, string>,
ResultAsync<number, number>,
ResultAsync<boolean, boolean>,
]

type ExpecteResult = Result<[ string, number, boolean ], [string, number, boolean]>
const heterogenousList: HeterogenousList = [okAsync('Yooooo'), okAsync(123), okAsync(true)]

type ExpecteResult = Result<[string, number, boolean], [string, number, boolean]>

const result: ExpecteResult = await ResultAsync.combineWithAllErrors(heterogenousList)

Expand Down Expand Up @@ -832,7 +826,6 @@ describe('ResultAsync', () => {
const okVal = okAsync(12)
const errorCallback = jest.fn((_errVal) => errAsync<number, string>('It is now a string'))


const result = await okVal.orElse(errorCallback)

expect(result).toEqual(ok(12))
Expand Down Expand Up @@ -959,7 +952,7 @@ describe('ResultAsync', () => {

return 12
})

const val = await example()
expect(val.isErr()).toBe(true)

Expand All @@ -969,9 +962,9 @@ describe('ResultAsync', () => {
it('Accepts an error handler as a second argument', async () => {
const example = ResultAsync.fromThrowable(
() => Promise.reject('No!'),
(e) => new Error('Oops: ' + e)
(e) => new Error('Oops: ' + e),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like prettier changes in this file.

)

const val = await example()
expect(val.isErr()).toBe(true)

Expand Down
26 changes: 0 additions & 26 deletions tests/tsconfig.tests.json

This file was deleted.

45 changes: 20 additions & 25 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,31 +1,26 @@
{
"compilerOptions": {
"target": "es2015",
"module": "ESNext",
"moduleResolution": "Node",
"strict": false,
"noImplicitAny": true,
"sourceMap": false,
"noUnusedLocals": true,
"noUnusedParameters": true,
"strictNullChecks": true,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slimmed down the tsconfig by flags inferred by other flags.

"strictFunctionTypes": true,
"declaration": true,
"baseUrl": "./src",
"lib": [
"dom",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the DOM types, not needed here.

"es2016",
"es2017.object"
],
"outDir": "dist",
"skipLibCheck": true,
"esModuleInterop": true
"target": "ES2015",
"module": "ESNext",
"moduleResolution": "Node",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"lib": [
"ES2016",
"ES2017.Object"
],
"skipLibCheck": true,
"esModuleInterop": true,
"noEmit": true
},
"include": [
"src/**/*.ts"
"src/**/*.ts",
],
"exclude": [
"node_modules",
"**/*.spec.ts"
"exclude": [],
"references": [
{
"path": "./tsconfig.tests.json"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now referenced in the root tsconfig, meaning both can be run simultaneously.

}
]
}
}
18 changes: 18 additions & 0 deletions tsconfig.tests.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noUnusedLocals": false,
"noUnusedParameters": false,
"declaration": true,
"composite": true,
"outDir": "./tests/dist",
"noEmit": false
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be "noEmit": true?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using project references means that you do need to generate declaration files. This acts as a kind of cache for TS, making things run a bit faster.

These files are outputted to ./tests/dist, as specified in outDir

},
"include": [
"./src/**/*.ts",
"./tests/**/*.ts",
],
"exclude": [
"./tests/dist"
]
}
Loading