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

feat: add a method for returning an object #10

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

henrycunh
Copy link

This will allow for handling errors or data using a single reference

This will allow for handling errors or data using a single reference
@bdsqqq
Copy link
Owner

bdsqqq commented Feb 26, 2023

Thanks for the PR!
Originally I chose a tuple so the consumer can name both values easily, but I like the idea of allowing the return to be an object.

That being said, I'm not 100% sold on having another function with an ambiguous name, both trytm and try$ don't say what they do. It might be fine considering the library's size, but I'd like to get more input about it before going forward.

A questionable alternative I thought about while writing this was having a config object as the second parameter of the function, I kind of hate it but will throw it here for the sake of discussion.

const data = trytm(promise(), {return: "object"})

const [dataB, err] = trytm(promise(), {return: "tuple"});

const [dataB, err] = trytm(promise());

This would add a couple of ifs inside the function and ship unnecessary code to users that just want one function or the other so I don't really like it. Additionally, users wouldn't be able to choose one or the other by default so they'd be forced to pass the second argument every time if they want the object. Again, don't really like this option...

What about having trytm as a wrapper for both and one as a default?

// dist.ts
const trytm = {
  tuple: functionThatReturnsTuple,
  obj: functionThatReturnsObj
}

// userLand.ts
import { trytm, tuplePlaceholderName, objPlaceholderName}

const data = trytm.obj(promise);

const dataB = objPlaceholderName(promise);

const [dataC, errC]= trytm.tuple(promise);

const [dataD, errD] = tuplePlaceholderName(promise);

const [dataE, errE] = trytm(promise); // would default to tuple to maintain compatibility?

This feels better than the other one I wrote above but I'm not 100% sold on this either.


In the end, I think it comes down to:

  • do we want more than one function in the lib?
    • if yes, should their names make sense, or is tm and similar fine?
    • if no, how do we go about surfacing multiple behaviors?

Again, thanks for the PR and I hope we can take this in a direction that makes the lib better :)

Any feedback on how to go about this is appreciated

@henrycunh
Copy link
Author

That being said, I'm not 100% sold on having another function with an ambiguous name, both trytm and try$ don't say what they do. It might be fine considering the library's size, but I'd like to get more input about it before going forward.

I totally agree with this, and part of the reason I implemented with this weird-ass name was to spark conversation about the exported methods' names.

Most of the time I implemented this exact pattern, I named the function handleError -- but I feel it's really common to name functions with this name, as there has been a couple times where I named it catchError.

My intuitive response would be call this catchError and make the return both a tuple and an object, by overriding the Symbol.iterator attribute:

export const trytm = async <T>(
    promise: Promise<T>,
): Promise<TryResult<T>> => {
    const getResult = (data: T | null, error: Error | null) => ({ 
        data, 
        error,  
        [Symbol.iterator]: function* () {
            yield this.data;
            yield this.error;
        }
    })
    try {
        const data = await promise;
        return getResult(data, null);
    } catch (throwable) {
        if (throwable instanceof Error) {
            return getResult(null, throwable);
        }
        
        throw throwable;
    }
};

This would allow for both return types on the same object:

const mockPromise = new Promise((resolve) => {
    setTimeout(() => {
        resolve({ hey: "Bedesqui" });
    }, 1000);
})

async function main() {
    const [data, error] = await trytm(mockPromise);
    const result = await trytm(mockPromise);
}

The only bad part about this is that when autocompleting the properties the return object has, it will show:

  • data
  • error
  • Symbol

Which is not pretty... wdyt?

@henrycunh
Copy link
Author

henrycunh commented Feb 27, 2023

Okay, got a solid implementation of this working

class TryResult<T extends any | null> {
    private [Symbol.iterator] = function* (): IterableIterator<[T | null, Error | null]> {
        yield [this.data, this.error];    
    }
    public data: T | null;
    public error: Error | null;

    constructor(data: T, error: Error | null) {
        this.data = data;
        this.error = error;
    }
}

export const trytm = async <T>(
    promise: Promise<T>,
): Promise<TryResult<T | null>> => {
    
    try {
        const data = await promise;
        return new TryResult(data, null);
    } catch (throwable) {
        if (throwable instanceof Error) {
            return new TryResult(null, throwable);
        }
        throw throwable;
    }
};

const mockPromise = new Promise<string>((resolve) => {
    setTimeout(() => {
        resolve('hello');
    }, 1000);
})

async function main() {
    const [[data, error]] = await trytm(mockPromise);
    const result = await trytm(mockPromise);
}

This solves Symbol appearing, but introduces you having to destructure like [[data, error]] to get the correct types :[

@bdsqqq
Copy link
Owner

bdsqqq commented Feb 28, 2023

I'm loving the direction this is taking, I'm sold on having both possibilities returned by the same function.

I'll try to find a way to not have the Symbol in autocomplete. Also thank you so much for exploring these ideas! IMO, between it and the [[ ]] I'd take the Symbol.

@henrycunh
Copy link
Author

Ha, what a puzzle! The only way I could get it working with both returns with correct return types is with this implementation

class TryResult<T extends any | null> {
    [Symbol.iterator] = function* () {
        yield this.data
        yield this.error    
        return this.error
    }
    public data: T | null;
    public error: Error | null;

    constructor(data: T, error: Error | null) {
        this.data = data;
        this.error = error;
    }
}

type TryMixedResult<T> = [T | null, Error | null] & TryResult<T>;

export const trytm = async <T>(
    promise: Promise<T>,
): Promise<TryMixedResult<T>> => {
    try {
        const data = await promise;
        return new TryResult(data, null) as any;
    } catch (throwable) {
        if (throwable instanceof Error) {
            return new TryResult(null, throwable) as any;
        }
        throw throwable;
    }
};

const mockPromise = new Promise<string>((resolve) => {
    setTimeout(() => {
        resolve('hello');
    }, 1000);
})

async function main() {
    const [data, error] = await trytm(mockPromise);
    const result = await trytm(mockPromise);
}

But the autocomplete of result is now complete junk, and trying to Pick<T,U> or Omit<T,U> makes the return of the tuple not work, starting to think it isn't possible...

image

@bdsqqq
Copy link
Owner

bdsqqq commented Mar 1, 2023

Frame 24

@bdsqqq bdsqqq mentioned this pull request Mar 2, 2023
@arn4v
Copy link

arn4v commented Mar 2, 2023

Here's an alternative approach:

type SuccessResult<T, _E> = [T, null] & {
    data: T;
    error: null
}
type ErrorResult<_T, E> = [null, E] & {
    data: null;
    error: E
}
type TryResult<T, E> = SuccessResult<T, E> | ErrorResult<T, E>

async function tryTm<T, E = Error>(promise: Promise<T>): Promise<TryResult<T, E>> {
    try {
        const data = await promise;
        const returnable = { data, error: null } as SuccessResult<T, E>;
        returnable[0] = data;
        returnable[1] = null;
        return returnable
    } catch (throwable) {
        const error = throwable as E;
        const returnable = { data: null, error } as ErrorResult<T, E>;
        returnable[0] = null;
        returnable[1] = error;
        return returnable
    }
}

const mockPromise = (async () => {
    return { greeting: 'Hello' }
})()

async function test() {
    const result = await tryTm(mockPromise)
    const { data, error } = result;
    const [data2, error2] = result;
}

https://www.typescriptlang.org/play?#code/C4TwDgpgBAygrgYwRAzigSquAbYAeAFQBooB9AUQD4oBeKAbWKgDsdsBdKAMigG8AoKEKgATAIbAxALigEA3IOEQATsoD2yma2zZ+AX36hIUcqo2YUOfKSZVaDbdhLlOPAcNETpLNgo8r1TRN9Q3BoAmUQCytCZ2o6eCRUDCxcWJNqAB8TM2VotNtKfn4xFBBmBCgAMzgK4ABLNWYoYEiCAFt08ntTQMoACjB1dvqUCBkABWHRiEJKAEpJ6bHCSPz8Qup3YVaQPkUPIQQmlGBPSXsxAHcxerOhtRGxv0Ojk7PlCGA4ZWYxACNsNA6LxzmISAENFo2FA9FBSrBEMg0OsupQXq9Pt9fgCgfQAAycOjiSQYw5Yn5-QEQegARiJPh0ZI8FN+UFZVKBB1hUAQEgQAAsoP1gAL1DdqfN9q83sxTlBIcp7KLxbjoAjyMzhMc5R8vpS1fZQSTvI4IbkeRrcqjClqhBy1QSGY47ez9TjqXSGYrXRy3djORBuQYDPwdfL2moEABrKaPGb2fqlcqVfpSmhbbl+0EAc0+X3qzBzMgA5AAJCA6NQl2H6eZp4rJirVWoIBpNFqoYBp6Xa95uyy4S43O4tNrtfqRmNxp4Qebc8NnY1ec2BHl0T6D4BkxcME0AJlXGn3DM3VgUeiAA

@henrycunh
Copy link
Author

henrycunh commented Mar 2, 2023

you're a BEAUTIFUL human being

@henrycunh
Copy link
Author

henrycunh commented Mar 2, 2023

oh, I just tested it; ends up with the same problem I bumped into before :[
image

@bdsqqq
Copy link
Owner

bdsqqq commented Mar 2, 2023

I think at this point we just accept the Symbol in autocomplete. It's ugly but it's technically supposed to be there... Since types are supposed to say the truth we shouldn't lie to our consumer about Symbol existing or not

@arn4v
Copy link

arn4v commented Mar 2, 2023

@henrycunh Try this

type SuccessResult<T, _E> = {
  data: T;
  error: null;
} & [T, null];
type ErrorResult<_T, E> = {
  data: null;
  error: E;
} & [null, E];
type TryResult<T, E> = SuccessResult<T, E> | ErrorResult<T, E>;

export async function tryTm<T, E = Error>(
  promise: Promise<T>,
): Promise<Omit<TryResult<T, E>, keyof typeof Array.prototype>> {
  try {
    const data = await promise;
    const returnable = [data, null] as SuccessResult<T, E>;
    returnable.data = data;
    returnable.error = null;
    return returnable;
  } catch (throwable) {
    const error = throwable as E;
    const returnable = [null, error] as ErrorResult<T, E>;
    returnable.data = null;
    returnable.error = error;
    return returnable;
  }
}

image

@NicolasLopes7
Copy link
Contributor

weird @arn4v just tested here and the array is not showing up, did you paste the right snippet of code?
Screenshot 2023-03-01 at 23 18 21

@arn4v
Copy link

arn4v commented Mar 2, 2023

Omitting keys of Array.prototype also omits Symbol.iterator so TS thinks its no longer destructureable as an Array. (I can still see access by index though result[0]/result[1])

I personally don't care about the autocomplete polution + it makes sense to let it be for technical correctness' sake.

Edit:

Found a feature request from 7 years ago that was rejected for the same reason (tuples are just arrays, so those methods are supposed to be there): microsoft/TypeScript#6325 (comment).

@nabeelvalley
Copy link

nabeelvalley commented Mar 2, 2023

random question as well, does it matter if you return a tuple or object? why not return an object that satisfies both and the user can just pick depending on how they use it?

may also make typing it more straightforward and keep backwards compatibility

(not too sure about the practicality of implementing that though)

@henrycunh
Copy link
Author

henrycunh commented Mar 2, 2023

I think at this point we just accept the Symbol in autocomplete. It's ugly but it's technically supposed to be there... Since types are supposed to say the truth we shouldn't lie to our consumer about Symbol existing or not

In the example I gave with this problem, the return signature of the tuples' items were T | Error | null so not perfect as well :[

So, summarizing alternatives:

  1. Having both items in the [data, error] type as T | Error | null
  2. Having polluted autocomplete
  3. Having to use [[data, error]]

Those are all the choices, I think

@bdsqqq
Copy link
Owner

bdsqqq commented Mar 7, 2023

This is not dead!

I'm focusing on another project rn but will come back to finish this soon™.


So, summarizing alternatives:

  1. Having both items in the [data, error] type as T | Error | null
  2. Having polluted autocomplete
  3. Having to use [[data, error]]

TBH I don't like any of these compromises so I'm leaning toward not implementing this into the lib, and instead adding a function that returns the desired object in the "Hey, this should be copy pasted instead of imported" section of the README.

Alternatively, I'm considering dropping the "a single function" part of the lib and exposing this as another import. Maybe something like:

import { trytm } from "@bdsqqq/try" // tuple return
import { trytm } from "@bdsqqq/try/tuple" // tuple return
import { trytm } from "@bdsqqq/try/obj" // obj return

This way we don't have to do any hacky types.

Let me know what you think!
and thanks all for the feedback and resources, I'm learning a lot through this PR <3.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants