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

fix(types): align isPromise return type with its logic #175

Merged
merged 4 commits into from
Aug 16, 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
20 changes: 16 additions & 4 deletions docs/typed/isPromise.mdx
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
---
title: isPromise
description: 'Determine if a value is a Promise'
description: 'Determine if a value is a Promise or has a `then` method'
---

### Usage

Pass in a value and get a boolean telling you if the value is a Promise. This function is not _"bullet proof"_ because determining if a value is a Promise in javascript is not _"bullet proof"_. The standard/recommended method is to use `Promise.resolve` to essentially cast any value, promise or not, into an awaited value. However, this may do in a pinch.
The `isPromise` function checks if a value is "Promise-like" by determining if it has a `then` method.

```ts
import * as _ from 'radashi'

_.isPromise({ then: () => {} }) // => true
_.isPromise(new Promise(() => {})) // => true
_.isPromise(Promise.resolve(1)) // => true
_.isPromise(Promise.reject(new Error('nope'))) // => true

_.isPromise('hello') // => false
_.isPromise(['hello']) // => false
_.isPromise(new Promise(res => res())) // => true
_.isPromise({}) // => false
```

This approach is useful for identifying objects that conform to the Promise interface without actually being instances of `Promise`. It's particularly helpful in scenarios where:

1. You need to quickly check if a value is thenable without resolving it.
2. Performance is critical, and you want to avoid the overhead of `Promise.resolve`.
3. You're working with custom Promise implementations or third-party libraries that use Promise-like objects.

While `Promise.resolve` is generally recommended for handling both Promise and non-Promise values uniformly, `isPromise` can be preferable when you need to make decisions based on whether a value is Promise-like without actually resolving or chaining it. This can be especially useful in type-checking scenarios or when implementing control flow that depends on whether a value is immediately available or needs to be awaited.
37 changes: 22 additions & 15 deletions src/async/tryit.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isPromise } from 'radashi'
import { isPromise, type Result, type ResultPromise } from 'radashi'

/**
* The result of a `tryit` function.
Expand All @@ -18,29 +18,36 @@ import { isPromise } from 'radashi'
* })
* ```
*/
export type TryitResult<Return> = Return extends Promise<any>
? Promise<[Error, undefined] | [undefined, Awaited<Return>]>
: [Error, undefined] | [undefined, Return]
export type TryitResult<
TReturn,
TError extends Error = Error,
> = TReturn extends PromiseLike<infer TResult>
? ResultPromise<TResult, TError>
: Result<TReturn, TError>

/**
* A helper to try an async function without forking the control flow.
* Returns an error-first callback-_like_ array response as `[Error,
* result]`
*/
export function tryit<Args extends any[], Return>(
func: (...args: Args) => Return,
): (...args: Args) => TryitResult<Return> {
return (...args) => {
export function tryit<
TArgs extends any[],
TReturn,
TError extends Error = Error,
>(
func: (...args: TArgs) => TReturn,
): (...args: TArgs) => TryitResult<TReturn, TError> {
return (...args): any => {
try {
const result = func(...args)
if (isPromise(result)) {
return result
.then(value => [undefined, value])
.catch(err => [err, undefined]) as TryitResult<Return>
}
return [undefined, result] as TryitResult<Return>
return isPromise(result)
? result.then(
value => [undefined, value],
err => [err, undefined],
)
: [undefined, result]
} catch (err) {
return [err, undefined] as TryitResult<Return>
return [err, undefined]
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/typed/isPromise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ import { isFunction } from 'radashi'
* isPromise(1) // => false
* ```
*/
export function isPromise(value: any): value is Promise<any> {
export function isPromise(value: any): value is PromiseLike<unknown> {
return !!value && isFunction(value.then)
}
10 changes: 6 additions & 4 deletions tests/typed/isPromise.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import * as _ from 'radashi'

describe('isPromise', () => {
test('return true for Promise values', () => {
expect(_.isPromise(new Promise(res => res(0)))).toBeTruthy()
expect(_.isPromise(new Promise(res => res('')))).toBeTruthy()
test('return true for Promise-like values', () => {
expect(_.isPromise(new Promise(() => {}))).toBeTruthy()
expect(_.isPromise(Promise.resolve(1))).toBeTruthy()
expect(_.isPromise((async () => {})())).toBeTruthy()
// biome-ignore lint/suspicious/noThenProperty:
expect(_.isPromise({ then: () => {} })).toBeTruthy()
})
test('return false for non-Date values', () => {
test('return false for non-Promise-like values', () => {
expect(_.isPromise(22)).toBeFalsy()
expect(_.isPromise({ name: 'x' })).toBeFalsy()
expect(_.isPromise('abc')).toBeFalsy()
Expand Down
Loading