binding icon indicating copy to clipboard operation
binding copied to clipboard

Feature: Update @computedFrom to support typing

Open silbinarywolf opened this issue 6 years ago • 9 comments

I'm submitting a feature request

  • Library Version: 1.7.3

Please tell us about your environment:

  • Operating System: Windows 10

  • Node Version: v8.11.1

  • Yarn Version: 1.5.1

  • Language: TypeScript 3.0

Current behavior: We do not get any type safety when using @computedFrom. A few blogs give users a method for extending @computedFrom manually, but why not just make this an upstream feature we can use?

What is the expected behavior? That @computedFrom can give us type safety out-of-the-box.

https://medium.com/tech-effectory/creating-a-typed-version-of-aurelias-computedfrom-decorator-with-typescript-27219651ecee

Example:

import {computedFrom as originalComputedFrom} from "aurelia-framework"; 
export function computedFrom<T>(...rest: Array<keyof T>) { 
  return originalComputedFrom(...rest); 
}
import {computedFrom} from './typedcomputedfrom'; 
export class App { 
	public foo = "a"; 
	public bar = "b";

	@computedFrom<App>("foo") 
	public get foobar () { 
		return `${this.foo} ${this.bar}`; 
	} 
}

What is the motivation / use case for changing the behavior?

  • More bugs can be caught at compile-time, this will reduce chance of "typo" mistakes when using @computedFrom
  • Rather than individual projects adopt this pattern and import a "fake" version of @computedFrom across their codebase, it's standard.

silbinarywolf avatar Nov 26 '18 23:11 silbinarywolf

@silbinarywolf Awesome issue. It would be nice if you could help update the typings here https://github.com/aurelia/binding/blob/master/src/aurelia-binding.d.ts#L877

bigopon avatar Nov 26 '18 23:11 bigopon

I experimented locally with putting this in as so: node_modules\aurelia-binding\dist\aurelia-binding.d.ts

/**
* Decorator: Indicates that the decorated property is computed from other properties.
* @param propertyNames The names of the properties the decorated property is computed from.  Simple property names, not expressions.
*/
export declare function computedFrom(...propertyNames: string[]): any;

/**
* Decorator: Indicates that the decorated property is computed from other properties. Adds compile-time type safety to @computedFrom.
* @param propertyNames The names of the properties the decorated property is computed from.  Simple property names, not expressions.
*/
export declare function computedFrom<T>(...propertyNames: Array<keyof T>);

However, what I found was that keyof doesn't work with private members. So now Im not so sure on this feature as I wouldn't want inexperienced programmers to think they should make various members "public" because that gives less errors.

silbinarywolf avatar Nov 27 '18 00:11 silbinarywolf

There's a thread discussing the problem in detail here: https://github.com/Microsoft/TypeScript/issues/13543

silbinarywolf avatar Nov 27 '18 00:11 silbinarywolf

Oh nice find. We can come back to this later. One thing we need is to ensure backward compatibility, so it would be

export declare function computedFrom<T = any>(...propertyNames: Array<keyof T>);

bigopon avatar Nov 27 '18 00:11 bigopon

Well, the way I've posted above is backwards compatible as well, it just has two-definitions, one without the polymorphic parameter and one with it :)

If your version has no backwards compatibility breaks, then I'd go with that :)

silbinarywolf avatar Nov 27 '18 00:11 silbinarywolf

Nice. I didn't notice that. Thought it was for diffing

bigopon avatar Nov 27 '18 00:11 bigopon

I also want to point out that computedFrom also supports sub properties like @computedFrom('sub.value'). It should be supported by the typing if you want to go that way.

qraynaud avatar Sep 08 '19 19:09 qraynaud

The problem with private members can be solved by using an expression instead of keyof. E.g.

class Example {
  private secret: string;
  @computedFrom<Example>(x => x.hidden)
  public get wisperedSecret(): string { return `Psst! ${this.secret}`; }
}

As long as the expression is typed inside the scope of the class itself, like above, you can access private members as expected.

I'm already using something similar today, where I use a Proxy<> to walk the expression chain to turn it into a string, so it also works with x => x.foo.bar.baz for example.

AnorZaken avatar Jul 04 '22 23:07 AnorZaken

@AnorZaken Looks great! 🚀 Do you mind sharing the solution using Proxy<>?

anton-gustafsson avatar Jan 23 '24 13:01 anton-gustafsson