-
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
Suggestion: union types #14
Comments
👍 |
From #186; an implementation that leverages the existing lookup power of objects may also ber possible/simpler. Here's what I'm thinking of...
That would approximately correspond to a JS object that is assumed to have exactly one of the following parameters, i.e. be treated a bit like:
When you define a variable of this type, e.g.
it can be compiled like so:
Then you can match in a more standard functional programming style of syntax like so:
Which would be compiled to JS like so:
That seems to provide reasonable balance of conciseness, readability and efficiency. |
👍 |
1 similar comment
+1 |
👍 |
1 similar comment
+1 |
I like the union style that looks like an interface declaration, but inline I'd prefer var value: Promise<number> | number;
// instead of
var value: union { Promise<number>; number; }; |
We could have |
It's extremely difficult to properly type the constructor for an ES6 Promise. In ES6, the Promise constructor takes a callback that receives two arguments: a resolve function and a reject function. If the resolution value passed to the resolve function is itself a Promise or a "thenable", the final state of the resolution value will be adopted by the new Promise. This is difficult to properly type in TypeScript today: declare class Promise<T> {
constructor(init: (resolve: (value?: T) => void, reject: (reason: any) => void) => void);
constructor(init: (resolve: (value: Promise<T>) => void, reject: (reason: any) => void) => void);
} With the above, you can use contextual typing for the resolve callback to pick the correct constructor overload, but not type inference: // `resolve` is contextually typed
// result: ok
new Promise<number>((resolve: (value: number) => void) => {
resolve(0);
});
// `resolve` is contextually typed
// result: ok
var p: Promise<number>
new Promise<number>((resolve: (value: Promise<number>) => void) => {
resolve(p);
});
// `resolve` is inferred from the first constructor
// result: ok
new Promise<number>(resolve => {
resolve(0);
});
// `resolve` is inferred from the first constructor
// result:
// error TS2082: Supplied parameters do not match any signature of call target:
// Could not apply type 'number' to argument 1 which is of type 'Promise<number>'
// error TS2087: Could not select overload for 'call' expression
new Promise<number>(resolve => {
resolve(p);
});
// `resolve` is inferred from the first constructor
// result:
// error TS2082: Supplied parameters do not match any signature of call target:
// Could not apply type 'number' to argument 1 which is of type 'Promise<number>'
// error TS2087: Could not select overload for 'call' expression
var a = true;
new Promise<number>(resolve => {
if (a) {
resolve(0);
}
else {
resolve(p);
}
}); As a result, the only way to make type inference work here is to add another overload that uses an inline interface with multiple call signatures: declare class Promise<T> {
constructor(init: (
resolve: {
(value?: T): void;
(value: Promise<T>): void;
},
reject: (reason: any) => void
) => void);
constructor(init: (resolve: (value?: T) => void, reject: (reason: any) => void) => void);
constructor(init: (resolve: (value: Promise<T>) => void, reject: (reason: any) => void) => void);
} However, even with the above, you still cannot mix both contextual typing of declare class Promise<T> {
constructor(init: (resolve: (value?: union { T; Promise<T> }) => void, reject: (value: any) => void);
} |
@ivogabe, Generally I think having a single syntax would be easier to digest for users in the long run. There was a reason I settled on the |
Type Unions only go part of the way to solving issues with proper typing for things like ES6 Promises. Due to the fact that ES6 Promises adopt the state of a "thenable" resolution value (in essence, recursively unwrapping "thenables" until reaching a non-"thenable" or a rejection), you can never have an ES6 Promise with the type: // p is typed as a Promise<Promise<number>>, but is really just a Promise<number>
var p = promise.then<Promise<number>>(() => ...); I had additionally proposed a new constraint not for generics that could solve this: // ES6 "thenable"
interface Thenable<T not Thenable> {
then<TResult not Thenable>(
onFulfilled?: (value: T) => ThenableResult<TResult>,
onRejected?: (reason: any) => ThenableResult<TResult>
): Thenable<TResult>;
}
// ES6 "thenable" result
interface ThenableResult<T not Thenable> extends union {
T;
Thenable<T>;
}
// ES6 Promise
declare class Promise<T not Thenable> implements Thenable<T> {
constructor(init: (resolve: (value?: union { T; Thenable<T>; }) => void, reject: (reason: any) => void);
...
then<TResult not Thenable>(
onFulfilled?: (value: T) => ThenableResult<TResult>,
onRejected?: (reason: any) => ThenableResult<TResult>
): Promise<TResult>;
...
} Here, T can be any value that does not match the interface of Thenable<{}>, which would disallow |
+1 |
1 similar comment
+1 |
The slides at 1:37:00 in this video on Facebook's Flow type checker shows a little into how union types are handled there: https://www.youtube.com/watch?v=Bse0sYR7oVY#t=4139 The Flow type annotations are very similar to TypeScript so it might be a good reference. The cool part is that you can write regular JavaScript ( |
@robertknight nice. More direct link : http://youtu.be/Bse0sYR7oVY?t=1h37m7s |
But you can create same function in TS with multiple different type syntaxes? |
Thanks @basarat / @robertknight - nice to see! |
@Thorium, if you use function overloads for this purpose, then their number explodes exponentially.
And here's the way to do it with overloads:
Add another parameter with two possible types, and you get yourself 8 overloads. Fourth parameter - and it's 16. And so on. This is equally true if you pack these parameters in an interface - except now you also get to define 4 (or 8, or 16) different interfaces, one for every possible combination of types. And keep in mind that it is a very common pattern to have option bags where every parameter can have different types. Take jQueryUI's position helper for example - pay attention to the |
I said above that Flow type annotations were similar to TS. What I should have said is that they are syntactically compatible where the feature sets overlap. Flow also has non-nullable types and unions. |
Great discussion. We have a "spec preview" up at #805 that incorporates this feedback and puts up a concrete proposal to work from, so let's continue over there. |
FYI, Facebook just announced Flow language. It supports Union Types, and here's an excerpt that differentiates it from TypeScript:
|
TypeScript will soon support Union types, soon (http://blogs.msdn.com/b/typescript/archive/2014/11/18/what-s-new-in-the-typescript-type-system.aspx). However, I do appreciate hearing about Flow. In my experience, the type inference of TypeScript’s design-time and compile-time modes is already powerful enough for my needs. In fact, many beneficial features of TypeScript, such as Interfaces, Generics (with constraints), Modules, and Enumerations, along with first-class VisualStudio support, npm/JAKE/Travis CI support, and acceptance by tool-makers, like Telerik, aren’t things I’m likely to find in Flow for a great deal of time. Your observation about established JS libraries requiring “significant additional effort” to statically type is patently correct. Thankfully, there has already been a monumental, and ongoing effort to do just that, with DefinitelyTyped. The advantage I see with Flow, however, is that it’s exposing more developers to the syntactical conventions established in TypeScript. Above that, my assessment of Flow is that it’s a less-mature version of TypeScript. We may not get full “nullable” type support, but literally everything else is worth the investment into TypeScript. Besides, I think, given that all primitives are nullable anyway, it’s the responsibility of the coder in JS to make value checking a priority, at runtime, where any ambiguity exists (say by a call from an externally written bit of JS code), and at compile time with tests. This seems to strike an ironic chord with the stated purpose of Flow: trusting the developer to write good code. In any case, I hope both projects help evolve JavaScript beyond ES6 with some common conventions and improved design-time safety, while simultaneously providing a better experience for JS development overall in the near-term. From: Omid K. Rad FYI, Facebook just announced TypeFlow language, and it supports Union Types. Here's an excerpt that differentiates it from TypeScript: Flow’s type checking is opt-in — you do not need to type check all your code at once. However, underlying the design of Flow is the assumption that most JavaScript code is implicitly statically typed; even though types may not appear anywhere in the code, they are in the developer’s mind as a way to reason about the correctness of the code. Flow infers those types automatically wherever possible, which means that it can find type errors without needing any changes to the code at all. On the other hand, some JavaScript code, especially frameworks, make heavy use of reflection that is often hard to reason about statically. For such inherently dynamic code, type checking would be too imprecise, so Flow provides a simple way to explicitly trust such code and move on. This design is validated by our huge JavaScript codebase at Facebook: Most of our code falls in the implicitly statically typed category, where developers can check their code for type errors without having to explicitly annotate that code with types. This makes Flow fundamentally different than existing JavaScript type systems (such as TypeScript), which make the weaker assumption that most JavaScript code is dynamically typed, and that it is up to the developer to express which code may be amenable to static typing. In general, such a design leads to reduced coverage: Fewer type errors are caught, and tools are less effective. While this is a reasonable choice for some code, in general such a design does not provide as many benefits as it could without significant additional effort. Still, Flow provides a simple way to switch to this weak mode of type checking where desired, which is typically useful when checking existing code. — |
In my opinion, and speaking from the perspective of web/mobile application development, the idea of catching errors introduced by The example given in @omidkrad's link is the following: function length(x) {
return x.length; // Compile time error, because null is an argument below
}
var total = length('Hello') + length(null); In the real world, we are more likely to see function length(x) {
return x.length;
}
var request = new XMLHttpRequest();
request.onload = function() {
var obj = JSON.parse(this.responseText);
length(obj); // The web service may always send a non-null response, or may not
};
request.open("get", "www.example.com/getFoo", true);
request.send(); The resolution of this problem requires knowledge of the web service's policy. In short, for web/mobile application development, data enters the system either over the wire or from user interaction. The nullability of this data is not easily predictable. |
Many values or parameters in JavaScript can be of more than one type. For example, a function might take an object where one of the properties can be either a string or a number, but not a function.
Comments from Ron Buckton in https://typescript.codeplex.com/workitem/1364
I'd like to see type annotations provide some support for a type union. For example:
ES6 Promise
ES6 Loader
When static type checking is performed, it is possible to have some type issues when explicitly providing generic type arguments and having the wrong type chosen, but this exists today without supporting type unions.
The other open issue is what to do with a local that is a type union: should it act as an
any
, a type that contains all of the members of both (or all) types in the union, or a type that only contains the members that are the same in both (or all) types in the union.An implicit or explicit type cast from a type union to one of the union types would pass without the need to perform an intermediate cast to , and an explicit type cast to a more specific type for any of the union types would likewise succeed without an intermediate cast.
Assignment to a local or field that is a type union would succeed as if it were either of the types (e.g. implicit or explicit type cast from a more specific type to a less specific type specified in the type union).
There is also a question on how to properly handle the intellisense for a symbol that uses a type union. It could either be represented as a number of overloads (similar to what would have to be typed today), or preserve the type union definition.
Union Declaration
Providing a typedef-like syntax for unions would also be useful to define a reusable definition:
This is roughly analogous to an interface that defines multiple call signatures:
Static Analysis
Adding type unions would require changes to the static type information to be supported. The primary goal of adding type unions is to help the compiler determine the best matching type for a call expression or return type expression. The following sections discuss various ways of handling static analysis of type unions.
Assigning to a Type Union
When assigning to an identifier that is annotated with a type union, passing a value as an argument to a function for a parameter that is a type union, returning a value from a function with a type union in its return type annotation, or type-casting a value to a type union, the type of the value being assigned or returned must be compatible (as either an exact match or a superset of type information) with one of the types defined in the type union.
For example:
Assigning from a Type Union
When assigning to another value from a type union or type-casting from a type union, the type of the value must be compatible (as either an exact match or a subset of type information) with one of the types in the union.
For example:
The text was updated successfully, but these errors were encountered: