-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Inference fails for return-type of type-union between class and generic type #2211
Comments
The compiler attempts to collapse union types down at various times as a performance optimization when the constituent types aren't meaningfully different (ex the trivial case is when the union type class Fud { bar: string; }
...
console.log(test.foo()); // ERROR: "Property 'foo' does not exist on type Fud|Decorated" In almost all cases empty type definitions lead to a bad time and should be considered an anti-pattern. |
Okay, so it's being collapsed because If I add a method to class Fud {
bar(): string {
return "BAR";
}
}
interface Decorated {
foo(): string;
}
function decorate<T>(object: T) : T|Decorated {
object['foo'] = function () {
return 'Hello';
};
return <T|Decorated> object;
}
var test = decorate(new Fud());
console.log(test.foo()); // ERROR If I hover over What gives? |
Right, because interface Decorated<T> extends T {
foo(): string;
} but we don't allow you to write this sort of declaration unfortunately. What you're writing is something closer to mixins, see: The workarounds available now are casting to the type you actually expect, or to use index access to get at the property: var test = decorate(new Fud());
var r = test['foo'](); // no error but r is 'any' |
Oh. Is that how it normally works? In PHP (with php-doc) a type-annotation like I would have thought the same to be the case in JS, where it's common to have an object of either one type or the other - so you'd have to perform run-time checks to see if it's actually one type, or the other, or both. To me, merging types in this manner is just sort of weird, really... because the result of this type merge operation is neither X, nor Y, nor both - it's some weird hybrid type comprised of the methods they happen to have in common. I don't understand the usefulness of this. If I wanted two types to have something in common, I would have them both implement an interface, so that what they have in common is well-defined. I always took the vertical pipe as meaning "type alternatives" - maybe PHP (php-doc) got it wrong? |
It sounds like PHP is using it to mean a combination of union and intersection types. The goal for TypeScript's union types is to increase expressivity but also increase safety. The result of the operation isn't some hybrid type, it is what you think: a type that is either an A or a B. What follows from that is the question of what can safely be accessed off of instances of types like this. You could say it's simply unsafe to access anything until you 'pick' which part of the union type this actually is. But that's unnecessarily restrictive. We can guarantee that it is safe to access a property that is on A and B (assuming compatible types of the respective properties). As you noted if you want to access something that is only available on A or only available on B then some additional checks are required to do so safely/correctly. This is what type guards are for in TypeScript. When you write your runtime checks the type system can interpret what is happening and narrow the union to only the relevant part(s). var test = decorate(new Fud());
// if this was allowed it's entirely likely you just wrote code that will be a null ref exception
test.bar().toString(); // error
// this is what you needed to write anyway to make sure your code wasn't buggy
if(test instanceof Fud) {
test.bar(); // ok, now we're sure this is something that has a ```bar``` member
} The value of union types would be far lower if there was no checking for valid property access, assignability, etc. |
I know it's late to bring this up, but... The choice of operator here, the vertical pipe, is inconsistent both with JSDoc and Google Closure Compiler, where e.g. The use of the vertical pipe in JS and in general means "or", but the meaning of it in TS is more like "and", e.g. a type containing interfaces present in "X and Y", so something like Neither JSDoc, Google Closure annotations or php-doc even provide a type operation like the one implemented by Typescript - what's referred to as a "type union" in these established standards is not what I was expecting, and I'm pretty sure I won't be the first to react with surprise. I am hard pressed to think of a scenario where I would ever use this feature. All I can think is that I don't think anyone has ever asked for the type operation implemented here to be added to either JSDoc, Closure compiler or php-doc, and I think with good reason - in JS (or PHP) variables do not have types, so having alternative types assigned to the same variable (or property or parameter) is going to be a common scenario, while types that have some public members in common is, well, already covered by interfaces in Typescript and PHP; it isn't necessary. Type alternatives, on the other hand, occur all the time in existing JS code, and decoration (as per my example above) is quite common as well - but it would appear there is still no way to describe those type relationships. (?) I would be surprised if I'm the only person who perceives this as a bug - it's a pretty big departure from something that is already well-known and established in other JS annotation systems.
I get your point, in theory - but in practice, if you have alternative types, you expect to have to do some type assertions before operating on them, and it rarely turns out to be a problem at all. My bigger point is that this is inconsistent with established tools - it feels awkward. And it doesn't address a very real and frequent scenario in JS, where objects get decorated with more properties and methods at run-time. Happens everywhere, all the time. We already had interfaces which can describe common properties just fine, and in recent versions you don't even have to explicitly implement interfaces, so there is already a much better way to handle this case. I don't know when I would use this feature or why. Maybe I'm missing something? |
The choice of operator here, the vertical pipe, is inconsistent both with JSDoc and Google Closure Compiler, where e.g. number|string is referred to as a "union", but means exactly what I thought it meant - basically type alternatives, "X or Y". No, JSDoc, Google Closure, and TypeScript all have the same meaning for |, along with many functional languages that also have the concept of union type using |. This definition is precisely why you see the behavior you do. Because it is X or Y, but not both, it is unsafe to do anything to it that isn't safe to do to both X and Y. var x: string|number;
var y = 1;
y = x; // error, unsafe
var z = y * x; // error, unsafe in JS (or PHP) variables do not have types They absolutely do have types. That you have not written them explicitly is a very different matter. If JS variables actually did not have types then things like TypeScript and Flow couldn't even exist. The fact that a single variable could be multiple types, or a type that can change at runtime, is precisely why we need union types to describe them statically, or intersection types to describe their dynamic behavior. having alternative types assigned to the same variable (or property or parameter) is going to be a common scenario Yes, this is precisely what union types are for and why they were commonly requested. I am hard pressed to think of a scenario where I would ever use this feature. See previous discussions/requests here: I get your point, in theory - but in practice, if you have alternative types, you expect to have to do some type assertions before operating on them, and it rarely turns out to be a problem at all. This is not what we've heard from our users or experienced ourselves in a variety of languages with this feature. More generally, we could remove all sorts of type safety checks by this logic. This argument feels like 'in practice, just remember to always use the correct types correctly and you don't need a type checker.' And it doesn't address a very real and frequent scenario in JS, where objects get decorated with more properties and methods at run-time Yes, that is a different feature, my earlier reply linked to a few issues already tracking suggested ways to implement it (mixins/decorators/etc). It is definitely on our radar. |
Just to clarify (so I don't sound like an idiot) that was a misphrase - what I meant to say, was that variables and members don't have defined types, meaning it's totally OK to do things like
Could have sworn I had done this in JSDoc at least. Guess not. So PHP got it wrong again. That's shocking. (but not really.) Anyway, I mucked about with type unions some more last night, and I do see their usefulness now - although I still don't agree with magically generating an interface based on whatever members happen to have the same signature, this is a feature I can simply avoid using. It's still useful as far as allowing more than one type as argument to a function, and in conjunction with Thank you for your patience! I really appreciate it. Typescript remains the most awesome thing since sliced bread :-) |
The case for using a union type as declared is quite limited: var stringOrNumber: string|number = 'string';
// Not very useful
stringOrNumber.toString(); The real utility comes from type guards if(typeof stringOrNumber === 'string'){
stringOrNumber.replace();
} I think there are still a few bugs to be fixed and extensions to be implemented. However, in practice we have found union types to be indispensable. |
Happy to talk through the issues :) I definitely get where you're coming from and in particular the decorator/mixin scenario is not handled by this stuff so it can seem odd if that was the direction you started from. although I still don't agree with magically generating an interface based on whatever members happen to have the same signature Don't think of it as magically generating a new interface. It's just about allowing the easiest possible use of a union type when it can be provided safely. It's annoying to have to write those runtime checks but sometimes it's a necessity. But there are cases where they're simply unnecessary to do certain things given the intersection of functionality of the union parts. With the examples we've looked at in this thread it's not necessarily super useful because you have fairly disjoint union parts (ex things like string|number, string|string[], etc) but it can be more useful if there's some overlap. Consider: class Animal {
name: string;
}
class Cat extends Animal {
name: string;
meow() {}
}
class Dog extends Animal {
name: string;
bark() {}
}
var cog: Cat|Dog;
console.log(cog.name); // ok, good, why should I have had to disambiguate Cat vs Dog to do this?
if(cog instanceof Cat) {
cog.meow(); // this on the other hand required more code to be safe
} I guess I'm not sure the preferred alternative you have in mind. We could have just made any property access on these types an error but that's not really better is it? |
I get that, but I don't agree that this is safe - which members happen to overlap is just cause and effect, it's not dependable, and we already have automatic (duck) type-casting for interfaces, so I don't see the utility at all. In your example, I would have never used It just seems like a poor alternative to interfaces, especially with auto duck casting (or whatever the correct term is) enabling us to specify a requirement only on the consumer side, rather than having to explicitly implement an interface.
Yes and no. Using unions in this way, nothing is precisely defined on neither the consumer or provider side - I don't see the value of that. It doesn't help me solve a problem, and doesn't help me writer better or more self-documenting code. All I can see this feature doing is helping me avoid using features that could have made my code better and safer. I'm not a fan. Sorry :-) |
To clarify with examples - if Dog and Cat were both animals, I would write:
In this case, the code indicates that we are interested in common traits of Animals, whereas the other way, it suggests we are interested in either a Cat or Dog specifically, which doesn't seem to matter here. If there was no common base class, I would do this:
In this case, the code explains which trait we're interested in, so at least the consumer side indicates what aspect of a Dog or Cat it is interested in. I'm pretty sure I will be using unions only to clarify in cases where only certain specific types are allowed - I don't think I could find a case where I wouldn't also want the code to explain why those types are allowed. Did somebody ask for this feature? Where was it used in real life? |
I get that, but I don't agree that this is safe - which members happen to overlap is just cause and effect, it's not dependable, and we already have automatic (duck) type-casting for interfaces, so I don't see the utility at all. It's precisely as safe as accessing any other members off of well defined types. If you change the types involved in a way that it no longer becomes safe to access it without disambiguating the union cases then the original access will become an error. It just seems like a poor alternative to interfaces function doStuff(x: string|number|myOtherType) {
console.log("The value is: " + x.toString());
// do some stuff, pass x to other functions, etc
} The correct parameter type is not If you just want to write a ton of interface definitions for every intersection of functionality in your program you are certainly more than welcome to do that. For many people this is both onerous and unnecessary, if not actually imprecise. var cog: Named;
console.log(cog.name);
if(cog instanceof Cat) {
cog.meow(); // this on the other hand required more code to be safe
} Take this code and put it in a function called WalkCatOrDog. What is the parameter type of cog? It is not Named. That is imprecise. The function is not intended to take anything with a name (like a function typed object). It is also not Animal, because I'm writing a function specific to ex how you walk a cat or dog. There's no reason for me to have to duplicate the code to log the name in every type guard. What if Animal has 6 properties? Are you going to write N interfaces for every combination of those 6 properties that could be shared through a union of multiple Animal types? Did somebody ask for this feature? Where was it used in real life? I already pointed out multiple threads where people asked for this and multiple libraries already using this feature to increase type safety and expressiveness. You certainly don't have to use this feature if you don't like it but it's been a top request for us for a long time. |
Type inference seems to fail when the return-type is a union between a class and a generic type.
I was hoping to use this pattern to be able to decorate an object (not a class) with traits/behaviors on a per-instance basis, in a type-safe manner, something along the lines of Scala's
with
operator.Best explained by example:
So anything that goes through the
decorate()
function comes out decorated with a new interface, which is made possible using a union between a variable type and some interface.Hovering over the call to
decorate()
, the return-type is correctly reported asDecorated | Fud
- however, when the result is assigned to thetest
variable, the inferred type isFud
, and the attempt to calltest.foo()
generates the error "property foo does not exist on type Fud".Am I missing something? Is there some reason this shouldn't work?
The text was updated successfully, but these errors were encountered: