gigobyte / purify

Functional programming library for TypeScript - https://gigobyte.github.io/purify/
ISC License
1.5k stars 58 forks source link

Guidance on Extending Classes #623

Closed TheWix closed 1 year ago

TheWix commented 1 year ago

Hello,

I am attempting to write a library to extend some of the classes with useful methods without resorting to inheritance, for example:

declare module "purify-ts" {
  export interface Either {
    fromPredicate: <L, R>(predicate: (v: unknown) => v is R, onLeft:(() => L)) => (v: unknown) => E<L, R> 
  }
}

const eitherFromPredicate = <L, R>(predicate: (v: unknown) => v is R, onLeft:(() => L)) => (v: unknown): E<L, R> => 
  predicate(v) ? Right<R>(v) : Left<L>(onLeft());

Either.prototype.fromPredicate = eitherFromPredicate;

What I am running into: image

image

I haven't tried extending classes like this before using declaration merging. I dunno if Typescript is getting confused because Either is both an interface representing an instance of an Either<L, R> as well as a declared object of type EitherTypeRef representing the static functions of EIther.

Any advice on this would be helpful, thank you!

gigobyte commented 1 year ago

There is no Either class in purify, if you look at the source you will see Either is just a plain object with functions which return instances of the Left and Right classes. This is for performance reasons.

Let me know if this works for you: https://github.com/gigobyte/purify/issues/242#issuecomment-691527110

TheWix commented 1 year ago

So, this got me close, but I could only get it to work with an instance like Right(""). I think it is because EitherTypeRef isn't exported. image

gigobyte commented 1 year ago

Oh, if you want to extend Either as in Either the purify-ts export, it's just an object, so there's not much benefit in doing so. One option is to do this:

import { Either as PurifyEither } from 'purify-ts/es/Either'

type EitherTypeRef = typeof Either

interface MyEitherTypeRef extends EitherTypeRef {
  fromPredicate: any
}

const Either: MyEitherTypeRef = {
   ...PurifyEither ,
   fromPredicate: () => {}
}

export { Either }

and then

import { Either } from 'your-library'

OR (my recommendation) just export fromPredicate as a helper function, there's no need for it to be inside the Either object.

TheWix commented 1 year ago

Yea, I have it as a helper function now. I just wanted to see if I could hang it off Either like the other constructor functions so they were all in one place instead of people having to remember whether it is an extension vs the original. C# has the concept of Extension Methods so that is what I was going for.

atomictag commented 1 year ago

I also had the need to extend Either / EitherAsync but mostly "instance" methods. I resorted to using a pattern similar to what purifree did (see here). My additions?

  interface Either<L, R> {
    // convert an Either into an EitherAsync in a chain
    toAsync(): EitherAsync<L, R>;
    // execute effect regardless of whether "this" is Left or Right (EitherAsync already has that)
    finally(effect: () => any): this;
    // assert some condition, return Left result if the predicate returns false (super handy for validation)
    assert(predicate: (value: R) => boolean, left: L | ((value: R) => L)): this;
    // minor helper to return Either<L, void> from a chain
    void(): Either<L, void>;
  }

  interface EitherAsync<L, R> {
    // as above, just async variant (the predicate is sync, tho)
    assert(predicate: (value: R) => boolean, left: L | ((value: R) => L)): this;
    // return a Left if a chain does not return within an allotted time   
    timeout(ms: number, left: L | (() => L)): this;
    // execute async effect (Promise or EitherAsync) regardless of whether "this" is Left or Right
    finallyAsync(cb: EitherAsync<unknown, unknown> | (() => PromiseLike<unknown>)): this;
  }

Thank you for this awesome library - it's a real pleasure to use (and I've grown to love how cool EitherAsync is!)

TheWix commented 1 year ago

@atomictag Ooh, didn't know about this library either! Yea, the issue is that EitherTypeRef isn't exported. I actually got it working by doing this:

import { Either as OrginalEither } from "../../node_modules/purify-ts/es/Either.js";

module "purify-ts" {
  export * from "purify-ts";

  export interface EitherTypeRef2 extends (typeof OrginalEither) {
    fromPredicate: <L, R>(predicate: (v: unknown) => v is R, onLeft: (v: unknown) => L) => (v: unknown) => OrginalEither<L, R>
  }

  export const Either: EitherTypeRef2;
  export type Either<L,R> = OrginalEither<L, R>;
}

At this point, I'll probably just create my own extension, though. I'll hav a look at purifree to see if it has some helpers I have been writing.