Skip to content

Commit

Permalink
fix(types): align isPromise return type with its logic (#175)
Browse files Browse the repository at this point in the history
  • Loading branch information
aleclarson authored Aug 16, 2024
1 parent ada3225 commit d6e0dff
Show file tree
Hide file tree
Showing 4 changed files with 45 additions and 24 deletions.
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

0 comments on commit d6e0dff

Please sign in to comment.