-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Support type-checking of computed properties for constants and Symbols #5579
Comments
@IgorMinar The tricky issue here is that during the binding phase of compilation we need to know the spelling of every symbol (so we can build symbol tables). However, we can't know the actual spelling of the string or symbol that |
This seems directly related to an issue I have here: #8099 |
@ahejlsberg would it be possible to type this with an annotation like I think there's an obvious problem with dealing with A weirder problem exists around the polyfilling of symbols with |
If there was something like a generic primitive type, |
Coming in with something related... @ahejlsberg you say that the compiler has a bit of a challenge with resolving the types that aren't on the global When we target |
The way it works today is by checking the text of the name to identify the property, that is exactelly "Symbol.iterator". if you change any thing about this it is not found. this is why it is important to know that you are using the global "Symbol" and not a local Symbol. in --t es5, the definition of Symbol does not exist, so the check does not happen. but that does not mean it "works". for instance, see: import sym from './Symbol';
var i: Iterable<string>;
i[sym.iterator] // not the same as Symbol.iterator.
import Symbol from `./Symbol`;
const iteratorSymbol = Symbol.iterator;
i[sym.iterator] // still not the same as Symbol.iterator. This issue tracks making it work, ideally by knowing the "identity" of a symbol and use that to define and access properties. there is more relevant discussion on why it works this way and what is missing in #2012 |
It's probably worth disambiguating this a bit:
I don't think we can track the first type there. The rest, though, should be doable. |
Ok, I understand now. I guess though, it is a but "surprising" that unlike most features in TypeScript which have some sort of the "trust me, I know what I am doing" flag. This particular one doesn't seem to have such an option. |
Partially related... The developer will define the symbol using a special TS syntax. TypeScript will convert it to an index assignment in the transpilled code. const mySymbol = Symbol('My Symbol!!!');
class MyClass {
@mySymbol(value1: string): number { // also supports modifiers.
// do something...
}
}
let myClass = new MyClass();
myClass.@mySymbol('abc'); // <- Intellisense + return type number. Of course there are issues, this is just a quick sample for an idea:
Since (1) is a big issue TypeScript can work with class MyClass {
@mySymbol(value1: string): number { // also supports modifiers.
// do something...
}
} Becomes: var MyClass = (function () {
function MyClass() {
}
MyClass.prototype[Symbol.for('TS_SYMBOL: 12.MyClass.mySymbol')] = function (value1) {
// do something...
};
return MyClass;
}()); As for (4), open for discussion :) Anyway, if this is possible it will make Symbols a great feature that can be used in public API's. |
I am running into this limitation in my current project. Specifically, I am implementing a JSON serializer that needs to retrieve information about models, such as their ID, attributes, relationships, etc. Instead of hard-coding the serializer to a particular model library, I wanted to support a serialization protocol object that any model instance can implement, using symbols. Unfortunately the inability to define an interface using a non-built-in The code looks something like this: namespace JSONAPISerializer {
export const PROTOCOL = Symbol("serializer-protocol");
export interface SerializerProtocol {
getType(): string;
getID(): string | number;
}
export interface Serializable {
[PROTOCOL]: SerializerProtocol;
}
} I want to be able to write a import { PROTOCOL, Serializable } from "json-serializer";
class Model implements Serializable {
[PROTOCOL] = {
getType() { ... },
getId(): { ... }
}
} In my case, I am happy to have the constraint of the symbol being |
The implications of this proposal and cross-class privacy interests me. In some more complex systems it may be nice to have a module with multiple classes that can call certain methods on each other, but not allow consumers access to those methods. A hidden symbol (hidden from consumers at least) could be an interesting solution to this. Take the following code: namespace X {
const privateMethod = Symbol()
export class A {
b: B
constructor (b) {
this.b = b
}
b (): string {
return this.b[privateMethod]()
}
}
export class B {
[privateMethod] (): string {
return 'yay!'
}
}
}
const b = new X.B()
const a = new X.A(b)
// Works
a.b()
// Auto-completion shows nothing when typing `b.` or `b[` |
This would make polyfilling proposed well known symbols for proposals like |
Unique Symbols certainly make access to methods harder but they can't be fully hidden from a consumer. Object.getOwnPropertySymbols will expose an symbols used as properties. They are better suited to prevent clashes without having to namespace properties. Closures are the only way to block access to something in Javascript. (AFAIK) +1 For supporting run-time created symbols in interfaces. Well-known symbols and |
This feature would also be helpful when working with React. A minimal example: import * as React from 'react';
import { ChangeEvent } from 'react';
interface LoginState { username: string; password: string; }
export class Login extends React.Component<{}, LoginState> {
public state: LoginState = { username: '', password: '' };
private onChange(event: ChangeEvent<HTMLInputElement>, property: keyof LoginState) {
this.setState({ [property]: event.target.value }); // this doesn't work!
}
public render() {
return (
<form>
<input value={this.state.username}
onChange={(e) => this.onChange(e, 'username')}/>
<input type="password"
value={this.state.password}
onChange={(e) => this.onChange(e, 'password')}/>
<input type="submit" value="Login"/>
</form>
);
}
} Relevant definition of class Component<P, S> {
setState<K extends keyof S>(state: Pick<S, K>, callback?: () => any): void;
} Right now the commented line in We currently bypass this by calling |
@johnsoft i am afraid this is a different request and not included in this issue. |
@mhegazy You closed issue #15534 as a dup of this one yesterday :). It seems to me it fits within "type-checking of computed properties for constants". As far as I can tell (and I could be wrong), #15534 is the root cause of the React issue above. What would be the best way for me to get the above issue on the typescript team's radar? |
i see. this issue are about using a computed property whose type is a single literal type, and that is constant. thus the compiler can make assumptions about the name/key of the property. The example in #15534 made it seem like that is what you were looking for. The |
Sure, I'll copy my example over to the previous issue. |
I would want to warn against Another workaround which does not change your JavaScript output is
|
@jacobrask For the above example, |
@johnsoft Your import * as React from 'react';
import { ChangeEvent } from 'react';
interface LoginState { username: string; password: string; }
export class Login extends React.Component<{}, LoginState> {
public state: LoginState = { username: '', password: '' };
private onChange<K extends keyof LoginState>(event: ChangeEvent<HTMLInputElement>, property: K) {
this.setState({ [property]: event.target.value });
}
public render() {
return (
<form>
<input value={this.state.username}
onChange={(e) => this.onChange(e, 'username')}/>
<input type="password"
value={this.state.password}
onChange={(e) => this.onChange(e, 'password')}/>
<input type="submit" value="Login"/>
</form>
);
}
} |
@isiahmeadows Doesn't work, unfortunately.
|
@johnsoft The way you were doing it before would've been not type-safe across fields, which is why I suggested the change. I didn't expect it'd solve your problem completely, but it'll make it easier to catch errors (and hit this bug). |
@isiahmeadows Right, I get it. It would matter in the case where |
Any update on this? I noticed I still cannot strongly type my non built-in symbol property declarations in 2.6 👎 |
On Angular we are considering using ES6 computed prototype methods as part of Angular’s component api but we realized that typescript doesn’t allow us to express this in a type-safe way.
I’m wondering if this is a feature that could be added to typescript or if there is a fundamental issue that would prevent typescript from supporting this. Example:
Where
onInit
is a const string or ES Symbol, andOnInit
is an interface with the signature of the[onInit]
method.onInit
is a an optional life-cycle hook that Angular will call if the component has this property name.The reason why we find this api style attractive is that it removes the possibility of name-collisions, which means that we are free to add support for more hooks in the future without affecting any existing components.
PS: ES Observables are already using Symbols to define the observable interface, so this feature would allow creation of Observable interface. I expect more libs will want to take advantage of this api style as computed property and Symbol support becomes better.
Related issue: #4653
The text was updated successfully, but these errors were encountered: