-
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
Function this
types
#6018
Comments
Can you add a following syntax? let o = {
data: 12,
...
h() { // this is inferred from the contextual type
console.log(this.data);
}
} |
That's an extension of open question (2) -- can the contextual type of an object literal be used to contextually type a function member? Seems like the answer should be yes but I need to work through the details still. |
Should I create an another issue? |
No, your example already works in Typescript, |
Thank you. |
👍 for seeing a well thought out proposal! Yay! |
I want an error for implicit |
I think the best thing to do is default to It's extremely common for interface members of function type to be implemented using class methods, which require that the correct
But it's also very common for interface members to be used as
By distinguishing these two situations based on method syntax vs. property syntax, both of them are conveniently supported, and we preserve the intuitive fact that in
Same should go for object literals I guess: |
@sandersn Nice work! However my personal belief is that the constraint for the thisArg should look like CallSignature: for example type SomeFn = MyClass::(a: number) => string
var mc: MyClass = ...;
var fn: SomeFn = ...;
console.log(mc::fn(123)); // ok
console.log(20::fn(123)); // error: type number is not compatible with type MyClass or type GetOne = <T>Array<T>::() => T;
var pop = <GetOne>Array.prototype.pop;
var shift = <GetOne>Array.prototype.shift;
var x = [1,2,3]::pop() // x is number
var y = ["a","b"]::shift() // y is string
var z = {x: 7}::pop() // error and even more class A { ... }
class B extends A { ... }
class C { ... }
type ASelfMethod = A::(x: number) => this;
var a: A, b: B, c: C, fn: ASelfMethod;
...
var a2 = a::fn(123) // a2: A
var b2 = b::fn(123) // b2: B
var c2 = c::fn(123) // error What do you think? Summoning @WebReflection, @bergus, @zenparsing, @rbuckton while trying to align with ES7 bind operator :: and #3508 |
I don't know if it's relevant for this case but regarding having a class with an class Handler {
handleEvent(e) {
let type = e.type;
this['on' + type[0].toUpperCase() + type.slice(1)](e);
}
} Any class Clicker extends Handler {
constructor(el) {
this.el = el;
this.counter = 0;
el.addEventListener('click', this);
}
onClick(e) {
e.preventDefault();
this.counter++;
console.log(this.counter);
console.log(this instanceof Clicker); // always true
}
} Apologies if this was unrelated but I've felt like it was a missing bit in the initial brainstorm about the Best Regards |
@WebReflection it is almost impossible to assign a correct type to the expression that involves free-form indirect member access. That is why I've meant only a syntactical issue. I prefer type MyMethod = MyClass::(x: number) => string over type MyMethod = (this: MyClass, x: number) => string What you do? |
if that's the problem you can use a switch statement but if it's about adding listeners, unless the operator goes out the way I've suggested, which is having About the Otherwise I see The switch statement could be another solution that would make the Apologies again if this was somehow a side argument/issue not strictly related. Regards |
I am somewhat concerned about I'd generally prefer some other type-only mechanism for identifying the type of function filter<T> Iterable<T>::(callback: (value: T) => boolean): Iterable<T> { ... } Although another approach might be the following, very C++ like example: function Iterable<T>::filter<T>(callback: (value: T) => boolean): Iterable<T> { ... } Another option would be to supply the type of function filter<T, this extends Iterable<T>>(callback: (value: T) => boolean): Iterable<T> { ... } The proposed implementation does seem to align somewhat with C# extension methods: static IEnumerable<T> Filter(this IEnumerable<T> source, Func<T, bool> callback) { ... } |
bound<Iterable<T>> function filter<T>(callback: (value: T) => boolean): Iterable<T> { ... }
bound<Iterable<T>> (callback: (value: T) => boolean): Iterable<T> => { ... } function filter<T>(callback: (value: T) => boolean) binds Iterable<T> returns Iterable<T> { ... }
(callback: (value: T) => boolean) binds Iterable<T> returns Iterable<T> => { ... } Just a brainstorming. |
@rbuckton I get what you are saying about I think of trying to align to the potential bind operator ( Of the suggestions you made, I found the one in the type parameter list the best in my opinion, but with one variation: function filter<T, this:Iterable<T>>(callback: (value: T) => boolean): Iterable<T> { ... } The only problem I see with that or your suggestion though, is that it seems to imply that two arguments are required on the invocation of the function: filter<string, ?>(cb); Also, how would the following be dealt with: function filter<T, this extends Iterable<T>, U>(callback: (value: T) => boolean, options: U): Iterable<T> { ... } |
I think it's rather subjective. I believe that this solution has several advantages
Let's list all alternatives I.
|
@falsandtru this types are planned to be behind a flag that prevents implicit any. It will not be the default, however, because @jeffreymorlan the syntax distinction does solve the problem nicely. It's a bit subtle but it might be needed in this case just to be usable. I'm afraid the syntax distinction would end up in tons of posts like "12 Secrets of Typescript Exposed". @ahejlsberg do you have an opinion on this? @rbuckton, @Artazor thanks for the detailed suggestions on syntax. I'll add them to the proposal when I have time. As you can see I hadn't thought too much about it previously--I just used what was convenient for prototyping. |
Disclaimer: I am not sure if this adds to the discussion or is about the same thing. i may have missed the point entirely. I apologize in advance it this is the case! The below example DOES work as expected though despite the TS compiler complaining about it. The idea here is for the compiler to know that Person.fromData() returns a Person instance rather than an any or Base instance.
Error:
For me personally, having the return type of "this" in the Person.fromData() method works well. Since the "this" referenced in the fromData method represents the Person class itself, the compiler should know that a return type of "this" means an object of the Person type is being returned. Similarly, "this" as an input param to the Base.deserialize method also works well as again, the this represents the Person class which is then new'd. |
@Think7, you're working with the classes' |
IV. ES7-bind inspired variant is also my preference. // Inline arrow function with this type specified:
function filter<T> Iterable<T>::(callback: Iterable<T>::(value: T) => boolean): Iterable<T> { ... }
// Arrow function type alias with this type specified:
export type FilterCallback<T> = Iterable<T>::(value: T) => boolean;
function filter<T> Iterable<T>::(callback: FilterCallback<T>): Iterable<T> { ... }
// Overriding previously defined this type inline:
function filter<T> Iterable<T>::(callback: SuperIterable<T>::FilterCallback<T>): Iterable<T> { ... }
// Function this type declaration:
export type MyCallback<T> = Iterable<T>::Function; I can only assume there will be corresponding compiler flag(s): implicit -1 for option II since it implies specialization might be available via type of |
Fixed by #6739 |
Could we discuss the syntax? The merged PR has adopted option I (from @Artazor) which is a first parameter named |
Do I understand correctly that with the removal of
Is there a follow-up issue for the call site and assignability checking? |
no, feel free to file one Also note that explicitly specifying the class MyClass {
public x = {y: 45};
blah(tihs:this) {
console.log(this.x.y);
}
}
....
f(); // error TS2684: The 'this' context of type 'void' is not assignable to method's 'this' of type 'MyClass'. |
@mhegazy, thanks for the explanation; it saved me a while digging through comments and/or code So if I specify the |
@Igorbek at least it's easy to type and read. I'm thinking they chose option 1 for those less accustomed to JavaScript. |
@mattmccutchen that's correct. Explicitly provided this is fully checked, but if you leave it off then it becomes any so that old code doesn't break. The now-removed strict-this flag just changed the default from any to void for functions and class-this for methods. As @mhegazy says, feel free to open an issue tracking the return of this flag. The remaining issues are slower compile time and backward compatibility, esp with DefinitelyTyped. |
@gwicksted it's easy to type but it's definitely not easy to read. It's so easy to mess with formal parameter - it would need to know that would be a special case and at call site that argument wouldn't need to be passed. |
Did we have any great alternative syntax? I haven't seen one. |
4 options listed here: #6018 (comment) by @Artazor. I'm fully for IV and III. |
I'm especially for the ES7 bind compatible version simply because it is future proofing and essentially transpiling which is more the spirit of TypeScript. |
I have already said that the bind-like version is my favourite, however I understand why the current syntax was chosen - it is much more simple to parse/implement, it allows ThisArg to be generic, and it ressembles C#, so I think @Igorbek we have little chances to make any influence on it. It is the battle: simplicity versus purity. Simplicity wins |
Also, unfortunately the ES7 bind proposal is still on stage 0. |
I have a question inspired by removed interface I {
f(this: this): void;
//f this::(): void; // wouldn't it be nicer and clearer? :)
}
class A implements I {
private a = 1;
f(this: this) {
//f this::() {
this.a.toFixed();
}
}
class B implements I {
f(this: this) { }
//f this::() { }
}
const a = new A();
const b = new B();
b.f = a.f; // error
const i: I = a; // allowed?
b.f = i.f; // ok?
b.f(); // boom! Theoretically, |
@Igorbek, I don't get an error with interface II {
i: number;
}
class AA implements II {
i = 12;
a = 1;
}
class BB implements II {
i = 13;
b = 1;
}
let z: (arg: II) => void = function(arg: AA) { }; // legal :(
z = function(arg: BB) { }; // also legal :(
z(new AA()); // legal, error at runtime As for syntax, try writing interface Function {
// ...
bind<T,U>(this: (this: T, ...args:any[]) => U, thisArg: T, ...args: any[]): (...args: any[]): U;
// vs
bind<T,U> T::(...args:any[]) => U::(thisArg: T, ...args: any[]): (...args: any[]) => U; I am not sure if this would parse, actually. Even for humans, it forms a garden path construction since you can't tell whether the first |
Required parenthess for the nested this-type would really help: interface Function {
bind<T,U> (T::(...args:any[]) => U)::(thisArg: T, ...args: any[]): void::(...args: any[]) => U;
}
// or
type Func<T, R> = T::(...args[]) => R;
interface Function {
bind<T,U> Func<T, U>::(thisArg: T, ...args: any[]): Func<void, U>;
} For me, even this syntax looks much better still. |
@sandersn thank you for clarification, that it's caused by parameter bivariance. But if we hadn't it, any usage of |
Would it make sense for module to return let a: Titanium.UI.View = Titanium.UI.createView(); Now in this line there 3 types , class Proxy
class Module extends Proxy
global var Titanium: Module
Titanium.UI: Module
class Titanium.UI.View extends Proxy
function Titanium.UI.createView() : Titanium.UI.View For now the way chosen to declare this in typescript is this. export module Titanium {
export interface Proxy {
addEventListener(name:string, callback: Function) : this
}
export module UI {
export interface View extends Proxy {
}
export function createView(): Titanium.UI.View
}
} This works really well except for the method I understand that Or maybe there is another way to declare such a structure? |
You could try merging an interface and a module. But I couldn't figure out how to nest Proxy inside Titanium using this method. Here's what I got: declare interface Pxy {
addEventListener(name: string, callback: Function): this;
}
declare namespace Titanium {
export namespace UI {
export interface View extends Pxy { }
export function createView(): Titanium.UI.View
}
}
declare interface Titanium extends Pxy {
something(foo: number): this;
}
/// in main.ts,
/// <reference path="titanium.d.ts" />
let t: Titanium = undefined; // not sure how to get an instance of Titanium.
t.something(12);
let view = Titanium.UI.createView(); A few things to note:
|
@sandersn Thanks a lot will try that! And sorry will put that discussion in stackoverflow. |
This is a proposal for the other half of this-types outlined in #3694. The first half -- class 'this' types -- was implemented in #4910.
Motivation
This-types for functions allows Typescript authors to specify the type of
this
that is bound within the function body. Standalone functions are an important part of Javascript programming, so Typescript's ability to check the type ofthis
will capture patterns that it does not today. This feature enables three main scenarios.Typescript currently sets the type of
this
toany
except in when checking method bodies, where it is the class'this
type. To be backward compatible, almost all of this feature will be hidden behind a--strictThis
flag at first. This is because some working Javascript patterns will not be legal until they are annotated.Examples
These examples assume that
--strictThis
is enabled. I'll add examples without--strictThis
later.Prevent incorrect assignment between callback functions and methods
Build new objects from existing functions and methods
Require a suitable 'this' for substituting the current one
For example,
Function.call
allows the caller to specify a newthis
. This results in an interesting type:Syntax
this
is an optional first argument to all functions and methods except for lambdas. This syntax is a good representation of Javascript's actual semantics, wherethis
is available inside all function bodies and is checked the same way as any other parameter once its type is known.Examples of the syntax:
Note that, although it is syntactically an argument,
this
is treated specially during checking and erased at emit.@Artazor and @rbuckton point out that
this
as argument may not be forward-compatible with future versions of Ecmascript. @rbuckton listed the alternatives proposed so far:function filter<T> (this: Iterable<T>, callback: (value: T) => boolean): Iterable<T>
function Iterable<T>::filter<T>(callback: (value: T) => boolean): Iterable<T>
function filter<T> Iterable<T>::(callback: (value: T) => boolean): Iterable<T>
function filter<T, this extends Iterable<T>>(callback: (value: T) => boolean): Iterable<T>
Semantics
The semantics fall into 3 areas
Function Body Checking
Function bodies are checked as if
this
were a normal parameter. References tothis
in the body are required to satisfy the type provided forthis
. Ifthis
is not provided:this
is the class'this
type for methods.this
is not bound for lambdas.this
is of typevoid
for functions (for backward compatibility).For unannotated methods and lambdas, the behaviour does not change from current Typescript. For unannotated functions, the void type has no members, so uses of
this
are essentially disallowed. This will have to change for "loose this" mode.Examples:
Call-Site Checking
Call sites check a
this
argument against the function or method'sthis
parameter. To determine thethis
argument:• If the call is of the form
o.f()
, the type ofthis
is the type ofo
.• If the call is of the form
f()
, the type ofthis
is void.For example:
The
this
parameter's type can be given, as in the previous example. If it is not, then, similarly to body checking, it is:this
for methods.void
for functions.It is important that methods cannot be called without an object as if they were standalone functions. Given the types above, normal assignability rules will ensure this. On the other hand, functions actually can be called as if they were methods -- they just happen not to refer to
this
. To support this, we add an exception to normal assignability rules that applies when calling a function as if it were a method.Specifically, when the callee's
this
type -- thethis
parameter -- is of typevoid
, a non-voidthis
type will satisfy it. Let's look at examples of the two cases:Here is an example using lambdas to build up an object literal:
Assignability Checking
The rules for assignability are similar to those for call checking. The only complication is that default types for
this
have to be determined for both the source and the target. Like call checking, they conspire to make both functions and lambdas assignable to methods, but methods not assignable to functions.Open questions
How should loose-this work?
Should function literal's
this
be contextually typed?It would make callback-like methods require no additional type annotations to be fully checked:
Notably, allowing the contextual type of an object literal to contextually type a function member would allow checked ad-hoc construction from motivating scenario (2):
What should
this
of interface methods use?@jeffreymorlan suggests using
this
for method-style declarations andvoid
for function-style declarations. This aligns nicely with the increasing rift in method-style versus function-style declarations in ES2015 and ES2016.This will still add a lot of
this
parameters to interfaces, but they will mostly be desired ones.What if an interface is merged with a class? Does this change anything?
Implementation Progress
A prototype is at sandersn/TypeScript/check-this-function-types. It checks function and methods bodies, call sites and assignability but does not erase the 'this' parameter during emit. It also stuffs 'this' into the parameter list whenever it's convenient. It is somewhere between strict-this and loose-this in this proposal.
Here's what's left to do:
The text was updated successfully, but these errors were encountered: