Skip to content

How to handle this when this is a function #22285

Description

@dagda1

I have created this playground that shows the problem.

The code looks like this:

export interface Extender<A extends { new (name: string): A }> {
  instance: (constructor: { new() }, methods?: {}) => void;
  for: (value: any) => A;
  symbol: Symbol;
}

export function extend<A extends { new(name: string) }>(
  Class: A
): A & Extender<A> {
  const name = Class.name;
  const Extended = <A & Extender<A>>Class;

  let symbol = Symbol(name);

  Extended.instance = function(constructor, methods) {
    constructor.prototype[symbol] = methods;
  };

  Extended.symbol = symbol;

  Extended.for = function _for(value: any): A {
    let i = value[symbol];

    return i;
  };  

  const properties = getOwnPropertyDescriptors(Class.prototype);

    Object.keys(properties)
    .filter(key => key != "constructor")
    .forEach(key => {
      Extended.prototype[key] = Extended.prototype[key].bind(Extended.for);
    });

  return Extended;
}

interface Interface<A> {
  doSomething: (left: A, right: A) => A;
}

const Implementation = extend(class Implementation<A> implements Interface<A> {
  doSomething(left: A, right: A) {
    let { doSomething } = this(left);

    return doSomething(left, right);
  }
});

const { doSomething } = Implementation.prototype;

Implementation.instance(Object, {
  doSomething(o1, o2) {
    let properties = Object.assign({}, propertiesOf(o1), propertiesOf(o2));
    return Object.create(getPrototypeOf(o1), properties);
  }
});

doSomething({o: 1}, {b: 2});

How do I handle this on line 44 of the playground

let { doSomething } = this(left);

the call to this will return the specific doSomething.

At the moment I get the error, this lacks a callable signature.

If I add the false this param like this:

doSomething(this: (a: A) => Implementation<A>, left: A, right: A) {

And if I then export the function

export {doSomething} = Implementation.prototype;

And I then try to use the function:

doSomething({o: 1}, {b: 2}); I get the error:

The 'this' context of type 'void' is not assignable to method's 'this' of type '(a: any) => Implementation'.

I realise this is quite contrived but is this too contrived for typescript?

Activity

  1. ghost added
    Needs More InfoThe issue still hasn't been fully clarified
    on Mar 2, 2018
  2. dagda1 commented on Mar 2, 2018

    @dagda1
    Author

    Andy (Andrewkraft) (@Andy-MS)

    Ok so here is a jsfiddle of plain js and here is the code:

    function extend(Class) {
      const name = Class.name;
    
      const symbol = Symbol(name);
    
      // callable `this` in  `Adder#add`
      Class.for = function _for(value) {
        return value[symbol];
      }
    
      Class.instance = function(constructor, methods) {
        constructor.prototype[symbol] = methods;
      }
    
      Class.symbol = symbol;
    
      const properties = Object.getOwnPropertyDescriptors(Class.prototype);
    
      Object.keys(properties)
        .filter(key => key !== 'constructor')
        .forEach(key => {
          // bind `for` so `this(a)` calls `Class.for` 
          Class.prototype[key] = Class.prototype[key].bind(Class.for)
        });
    
      return Class
    }
    
    const Adder = extend(
      class Adder {
        add(left, right) {
          const {
            add
          } = this(left);
          return add(left, right);
        }
      }
    )
    
    const {
      add
    } = Adder.prototype;
    
    Adder.instance(Number, {
      add(left, right) {
        return left + right;
      }
    });
    
    console.log(add(1, 2)); // 3
    
    Adder.instance(String, {
      add(left, right) {
        return left.concat(right);
      }
    });
    
    console.log(add("1", "2")); // "12"
    

    Class.for is what is is what gets called via this in this code

          const {
            add
          } = this(left);
    

    This will return the correct add dependant if it is a String or a Number.

    This is the code that makes this an alias to Class.for

      Object.keys(properties)
        .filter(key => key !== 'constructor')
        .forEach(key => {
          // bind for so `this()` calls `Class.for` 
          Class.prototype[key] = Class.prototype[key].bind(Class.for)
        });
    

    So back to the typescript example:

    it would be:

    export interface Adder<A> {
      add(left: A, right: A) => A;
    }
    
    const Adder = extend(
      class Adder<A> {
         add(this: (a: A) => Adder<A>, left: A, right: A): A {
           const {add} = this(left);  // will call Class.for for the specific implementation
           
           return add(left, right);
         }
      }
    );
    
    export { add } = Adder.prototype;
    

    So is this too exotic for typescript to recognise or can I somehow indicate that the this in { add } has this bound by the call in extend with the line Class.prototype[key] = Class.prototype[key].bind(Class.for)

  3. dagda1 commented on Mar 3, 2018

    @dagda1
    Author

    This is really great but one thing, typescript does currently not support a symbol as an indexer:

    So the only way is to give the member any as its type

    export class TypeClass<A> {
      readonly symbol: any;
    

    Or is there another way?

  4. dagda1 commented on Mar 3, 2018

    @dagda1
    Author

    Andy (Andrewkraft) (@Andy-MS) this has been extremely helpful. I'm going to close the issue but is it possible to explain this commented line:

        instance<J extends T>(type: Function, implementation: J) {
            type.prototype[this.symbol] = implementation;
        }
    

    Why is it not just T that is the generic argument?

  5. dagda1 commented on Mar 5, 2018

    @dagda1
    Author

    Andy (Andrewkraft) (@Andy-MS) one problem with this is approach is that it expects the first argument to be the type argument, i.e.

    return ((left: any, ...args: any[]) => {
                const impl = left[this.symbol] as T;
    

    but what if the type in question is not the first argument in question. Generic types do not exist at runtime in typescript as I've no idea what that would compile to.

    I guess I would need to pass something into the instance function to indicate what where to get the type from?

  6. locked and limited conversation to collaborators on Jul 25, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Needs More InfoThe issue still hasn't been fully clarified

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions