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

How to handle this when this is a function #22285

Closed
dagda1 opened this issue Mar 2, 2018 · 9 comments
Closed

How to handle this when this is a function #22285

dagda1 opened this issue Mar 2, 2018 · 9 comments
Labels
Needs More Info The issue still hasn't been fully clarified

Comments

@dagda1
Copy link

dagda1 commented Mar 2, 2018

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?

@ghost
Copy link

ghost commented Mar 2, 2018

In your example you grab doSomething directly off the prototype, so I think TypeScript is correctly giving you an error when it says The 'this' context of type 'void' is not assignable to method's 'this' of type '(a: any) => Implementation. Where would the callable this come from? Could you come up with an example that works in JavaScript at runtime first before trying to get it to type-check? Then try and come up with a minimal example that shows a problem with the types.

@ghost ghost added the Needs More Info The issue still hasn't been fully clarified label Mar 2, 2018
@dagda1
Copy link
Author

dagda1 commented Mar 2, 2018

@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)

@ghost
Copy link

ghost commented Mar 3, 2018

Basically, to allow you to implement it that way, there would have to be a mapped type that iterates over all methods and removes the this parameter. That's not currently possible.
It is strange to write class and this where this will be bound to a static function.
Here's maybe another way to implement what you want:

// Assumes `T` is a type containing only function signatures.
class TypeClass<T> {
    readonly symbol: symbol;
    constructor(name: string) {
        this.symbol = Symbol.for(name);
    }

    /**
     * Must manually supply T<sometype> as the type argument because we don't have
     * https://github.com/Microsoft/TypeScript/issues/1213
     */
    instance<J extends T>(type: Function, implementation: J) {
        type.prototype[this.symbol] = implementation;
    }

    // Unfortunately we need to pass in the keys here. Or you could try and get them from the first call to `instance`.
    getImpls(keys: ReadonlyArray<keyof T>): T {
        const out = {} as T;
        for (const key of keys) {
            out[key] = this.getImpl(key);
        }
        return out;
    }

    getImpl<K extends keyof T>(name: K): T[K] {
        return ((left: any, ...args: any[]) => {
            const impl = left[this.symbol] as T;
            return (impl[name] as any)(left, ...args);
        }) as any as T[K]
    }
}

interface Add<T> {
    add(left: T, right: T): T;
}
const adder = new TypeClass<Add<{}>>("Add");
adder.instance<Add<number>>(Number, {
    add: (left, right) => left + right,
});
adder.instance<Add<string>>(String, {
    add: (left, right) => left + right,
});

const { add } = adder.getImpls(["add"]);
console.log(add(1, 3));
console.log(add("1", "3"));

@dagda1
Copy link
Author

dagda1 commented Mar 3, 2018

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?

@ghost
Copy link

ghost commented Mar 3, 2018

If the symbol is dynamically created Symbol.for(name) I don't think we can support typed operations using it. Especially if you need to look up Number.prototype[symbol]. But there are some cases where we do support symbols, see #15473.

@dagda1
Copy link
Author

dagda1 commented Mar 3, 2018

@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?

@dagda1 dagda1 closed this as completed Mar 4, 2018
@ghost
Copy link

ghost commented Mar 5, 2018

T is Add<{}>, but when you're implementing it you want a specific type like Add<number>. (Would be easier with #1213)

@dagda1
Copy link
Author

dagda1 commented Mar 5, 2018

@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?

@ghost
Copy link

ghost commented Mar 5, 2018

You won't be able to do something like have sum([]) be 0 or "" depending on whether you expect a number or string, without adding something else at runtime. Depending on what you're trying to accomplish you might want to store the impl for each type as a whole and pass that in like sum(NumberAdd, []).

@microsoft microsoft locked and limited conversation to collaborators Jul 25, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Needs More Info The issue still hasn't been fully clarified
Projects
None yet
Development

No branches or pull requests

1 participant