-
Notifications
You must be signed in to change notification settings - Fork 12.8k
This typing with intersection types #6452
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
Comments
This matches the intended design. this types have the scope of a class/interface, and are not instantiated on every call. This has been discussed before in #4931. |
Thank you very much for the link! I missed that in my search to see if it had been discussed before. Please forgive my lack of expertise, but I am struggling to understand the explanation given on that linked issue.
If you (or anyone else reading this) have time, could you please:
I should also point out that (as I am sure you are aware) the semantics I desire are possible using non-member functions. In the below code snippets, the variables have the types I expect: class A {
a: number;
}
class B {
b: number;
}
function self_<T>(a: T): T {
return a;
}
let v1: A & B;
let v2 = self_(v1); // Type of `v2` is `A & B` And: interface Extendable {
}
interface B {
b: number;
}
interface C {
c: number;
}
function extend<S, T>(self: S, x: T): S & T {
return;
}
let a: Extendable;
let b: B;
let c: C;
let d = extend(extend(a, b), c); // Type of `d` is `Extendable & B & C` However, "this typing" appears to be largely motivated by "fluent APIs" and the above code removes this syntactic nicety. Particularly the second example gets ugly quite fast without method chaining. Do I understand correctly that "function declaration this type support" would allow me to get the fluent API syntax with the semantics I desire? This is a not-yet-implemented feature, right? Thanks again! |
I'm working on this-types for functions right now, but it's not done. The proposal at #6018 links to the branch where I'm working on it. To explain why declare function extend<S, T>(self: S, other: T): S & T;
interface Extendable<this> {
extend<T>(other: T): this & T;
} Notice that interface B {
b: number;
}
interface C {
c: number;
}
let a: Extendable<this=Extendable>;
let b: B;
let c: C;
let d1 = a.extend<B>(b); // --> this=Extendable
let d2 = d1.extend<B>(c); // --> this=Extendable, NOT Extendable & B Notice that calling This-types for functions (#6018) should solve this by letting you specify interface Extendable {
extend<S,T>(this: S, other: T): S & T;
} This type binds the type of the calling object of let d1 = a<A,B>.extend(b);
let d2 = d1<A & B, C>.extend(c); |
Thank you very much for the explanation! I know have a much better grasp of how the current system works. I am still a bit unsure about why it needs to work this way, rather than having the this type be determined at the method call or property access. Thank you also for the information about "this-types for functions". I look forward to being able to use them! :-) |
Intended design does not match the intuition. I too expected the |
Here is a bit longer use-case, my backend API generates a TypeScript interface that I can use directly in TS with type safety. This implementation, which I thought was intuitive, is not working: export interface BasePromise<T> extends PromiseLike<T> {
onDone(cb: (t: T) => void): this;
}
// This interface is generated from the backend Api errors
export interface ApiErrors {
onError(errorCode: "ValidationError", cb: (data: { fields : { [k: string]: string[] }, messages : string[] }) => void): this;
onError(errorCode: "NotFound", cb: (data: null) => void): this;
onError(errorCode: "NotAuthorized", cb: (data: null) => void): this;
onError(errorCode: "Forbidden", cb: (data: null) => void): this;
onError(errorCode: "UndefinedError", cb: (data: null) => void): this;
}
// These are implementation specific errors, not generated from backend
export interface BaseErrors {
onError(errorCode: "XhrJsonParseError", cb: (data: null) => void): this;
onError(errorCode: "XhrUnknownContentType", cb: (data: null) => void): this;
onError(errorCode: "XhrWwwAuthenticationFailed", cb: (data: { error: string, error_description: string }) => void): this;
}
export const request = <T>(path: string, method: "POST", body: Object | null): BasePromise<T> & BaseErrors & ApiErrors => {
// Implementation omitted
return null as any;
}
request("some/url", "POST", {})
.onError("ValidationError", () => {
// Do on validation error
// This causes ERROR
}).onError("XhrJsonParseError", () => {
// Do on parse error
})
.onDone(() => {
// Do on done
}); I found a workaround, but it is a mess of intersection types at export interface BasePromise<T> extends PromiseLike<T> {
onDone(cb: (t: T) => void): this & BaseErrors<T> & ApiErrors<T>;
}
// This interface is generated from the backend Api errors
export interface ApiErrors<T> {
onError(errorCode: "ValidationError", cb: (data: { fields : { [k: string]: string[] }, messages : string[] }) => void): this & BaseErrors<T> & BasePromise<T>;
onError(errorCode: "NotFound", cb: (data: null) => void): this & BaseErrors<T> & BasePromise<T>;
onError(errorCode: "NotAuthorized", cb: (data: null) => void): this & BaseErrors<T> & BasePromise<T>;
onError(errorCode: "Forbidden", cb: (data: null) => void): this & BaseErrors<T> & BasePromise<T>;
onError(errorCode: "UndefinedError", cb: (data: null) => void): this & BaseErrors<T> & BasePromise<T>;
}
// These are implementation specific errors, not generated from backend
export interface BaseErrors<T> {
onError(errorCode: "XhrJsonParseError", cb: (data: null) => void): this & BasePromise<T> & ApiErrors<T>;
onError(errorCode: "XhrUnknownContentType", cb: (data: null) => void): this & BasePromise<T> & ApiErrors<T>;
onError(errorCode: "XhrWwwAuthenticationFailed", cb: (data: { error: string, error_description: string }) => void): this & BasePromise<T> & ApiErrors<T>;
}
export const request = <T>(path: string, method: "POST", body: Object | null): BasePromise<T> & BaseErrors<T> & ApiErrors<T> => {
// Implementation omitted
return null as any;
}
request("some/url", "POST", {})
.onError("ValidationError", () => {
// Do on validation error
}).onError("XhrJsonParseError", () => {
// Do on parse error
})
.onDone(() => {
// Do on done
}); |
If you are using 2.0 beta (by running export interface ApiErrors {
onError<T>(this: T, ...): T
} To boil down the problem into a single sentence: if you want a type, even a |
@sandersn yes it works! How come you call this workaround? Isn't this the solution? Following example can be copy & pasted to the VSCode: export interface BasePromise<TRes> extends PromiseLike<TRes> {
onDone<T>(this: T, cb: (t: TRes) => void): T;
}
// This interface is generated from the backend Api errors
export interface ApiErrors {
onError<T>(this: T, errorCode: "ValidationError", cb: (data: { fields : { [k: string]: string[] }, messages : string[] }) => void): T;
onError<T>(this: T, errorCode: "NotFound", cb: (data: null) => void): T;
onError<T>(this: T, errorCode: "NotAuthorized", cb: (data: null) => void): T;
onError<T>(this: T, errorCode: "Forbidden", cb: (data: null) => void): T;
onError<T>(this: T, errorCode: "UndefinedError", cb: (data: null) => void): T;
}
// These are implementation specific errors, not generated from backend
export interface BaseErrors {
onError<T>(this: T, errorCode: "XhrJsonParseError", cb: (data: null) => void): T;
onError<T>(this: T, errorCode: "XhrUnknownContentType", cb: (data: null) => void): T;
onError<T>(this: T, errorCode: "XhrWwwAuthenticationFailed", cb: (data: { error: string, error_description: string }) => void): T;
}
export const request = <T>(path: string, method: "POST", body: Object | null): BasePromise<T> & BaseErrors & ApiErrors => {
// Implementation omitted
return null as any;
}
request("some/url", "POST", {})
.onError("ValidationError", () => {
// Do on validation error
}).onError("XhrJsonParseError", () => {
// Do on parse error
})
.onDone(() => {
// Do on done
}); I also changed my above, not working examples to such they can be pasted to VSCode. |
Just tried this out in 2.0 beta myself. Very happy with the result! Just need to add the extra type parameter and explicit this parameter, and everything works as expected! Thanks for your work on this @sandersn, unless you see any reason to do otherwise I'm happy for you to close this issue now. interface Extendable {
extend<S, T>(this: S, x: T): S & T;
}
interface B {
b: number;
}
interface C {
c: number;
}
let a: Extendable;
let b: B;
let c: C;
let d = a.extend(b).extend(c); // 'd' has type 'Extendable & B & C' as desired :-) |
I am unsure if this is a bug, intended behavior or not yet implemented. Please advise.
Simple Example
I am using TypeScript 1.7.5 which has support for "intersection types" and "this typing". In the below code, I would expect the type of
v2
to beA & B
. However it actually has typeA
.More Useful Example
In the code below I would expect
d
to have typeExtendable & B & C
. However the type is actually justExtendable & C
.The text was updated successfully, but these errors were encountered: