-
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
Treat in
operator as type guard
#10485
Comments
interface A {
x: number;
}
interface B {
y: string;
}
interface C {
x: string;
}
let q: A | B | C = ...;
if ('x' in q) {
// q: A or C
// q.x ?
} else {
// q: B
} What would be the type of |
if ('x' in q) {
// q: A | C
// q.x: number | string
} else {
// q: B
} |
My bad.. 👍 |
This only makes sense if the interface B has some way to declare that it does not have the property class C implements B {
y = 'foo';
// this private method makes the type guard consider objects of this class to be an A, and not a B
private x() { ... }
} |
@jeffreymorlan While that's true, in practice people write user-defined type predicates all the time which basically assume that things are effectively sealed. |
That should be expressed in the declaration; otherwise you're constantly forced to consider whether an interface is sealed or not. Avoiding that kind of ad-hoc mental typing is the main reason for TypeScript to exist at all. |
@jeffreymorlan The guard in question have no info on |
😜 |
@aravindarun It breaks at run time: let q: A | B;
q = new C(); // allowed because C is assignable to B
if ('x' in q) { // this returns true at runtime
// compiler would think q is an A here - it is not
q.x.toFixed(); // fails at runtime, because q.x is a function, not a number
} If the interface Otherwise, this adds a major "gotcha" to the language. You won't be able to assign any object to an interface type without thinking about whether the interface is sealed or not. And since people will inevitably forget that, in existing code you won't be able to add any property, even a private one, without thinking if the name might clash with some other interface's tag. |
@jeffreymorlan this line of argument isn't really a productive one because people have been writing code with this "hole" for years already. See the code in #10421 -- it could fail in exactly the same way you're describing. In fact, there are many trivial ways to create unsound programs that fail to meet their types at runtime, it's just that in practice no one aggressively tries to break themselves. |
if ('x' in q) { // this returns true at runtime
// compiler would think q is an A here - it is not
q.x.toFixed(); // fails at runtime, because q.x is a function, not a number
}
Well, it better be (since @RyanCavanaugh +1 for the last comment. |
I think this is a duplicate of #1427. |
thanks @ethanresnick, This issue seems to have more up-to-date discussion. closing #1427 in favor of this issue. |
Is there a preferred alternative that folks are using here? I understand that "exclusive" unions and sealed types are theoretically thorny, but it's also the natural paradigm my brain suggests when trying to do runtime determination of configuration options, like so: interface ConfigObject {a: any;};
interface ConfigAlternative {b: any;};
export class Foo {
constructor (config : ConfigObject | ConfigAlternative) {
if ("a" in config) {
this.a = (<ConfigObject> config).a;
} else {
this.b = (<ConfigAlternative> config).b;
}
}
} (As you can see I'm currently getting around it with assertions.) In such a simple example I could just do away with the conditionals and just use Is there another paradigm I should be using? I'd prefer not to use real classes because it would require adding a great number of imports (including some detangling of cycles) and it seems overkill for such short-lived literals. EDIT: I guess I can be writing my own type predicates that are internally backed by |
@tdsmithATabc: as far as an alternative, as I commented in #13695 you can define a generic type guard that works for any property: export function hasKey<K extends string>(k: K, o: {}): o is { [_ in K]: {} } {
return typeof o === 'object' && k in o
} then use it like so, for example: type Foo = { x: number } | { y: string }
function f(foo: Foo) {
if (hasKey('x', foo)) {
console.log(foo.x + 5)
} else {
console.log(foo.y.length)
}
} |
Ahh thanks, I haven't started using mapped types because my work environment isn't on 2.1 yet. But that's pretty slick. 😄 |
Might we do the same for interface A {
x: number;
}
interface B {
y: string;
}
let q: A | B = ...;
if (q.x) {
// q: A
} else {
// q: B
}
// and also:
if (typeof q.x !== 'undefined') {
// q: A
} else {
// q: B
}
// and:
if (q.hasOwnProperty('x')) {
// q: A
} else {
// q: B
} |
Well, to reiterate what others said above: interfaces make no assertion that an object doesn't have additional members. Because of this, there's all sorts of "deficient" situations: class B2 {
public x : boolean = false; // does not satisfy A!
public y : string = "~";
}
const q : A | B = new B2();
if (q.x) {
// q is NOT A!!
} else {
// q: B
} People have mentioned run-time manipulations that make this trickier too. |
@tdsmithATabc I think that's a different problem. It shouldn't be possible to cast B2 to Therefore If there was a type guard in place preventing this type of union casting of incompatible types, the problem would not occur. |
I would like to see #1260 included as part of this. |
@niieani I think you're creating a different operator than union here (a partial of an intersection?). How would you handle literals with that logic? Nothing could ever "satisfy" Plus, what about types that can't be determined at compile-time? const truth : string | boolean = Math.random() > 0.5 ? "true" : true; I agree my constant example was poor because it could be immediately narrowed, but that doesn't change the general case of how interfaces and unions currently work. |
This all said, separating the issue of XOR-unions and member-exclusion-types, I would still like to see |
Inspired by @pelotom, changing the hasOwnProperty definition to: hasOwnProperty<V extends PropertyKey>(v: V): this is { [_ in V]: any } Would make this work for Or someone could add: interface Object {
hasOwnProperty<V extends PropertyKey>(v: V): this is { [_ in V]: any }
} to their code. |
Something similar could also be done for Reflect.has. |
Testing out the |
@RyanCavanaugh is this issue opened for community(can't see it in the community milestone)? class A { a: string; }
class B { b: string; }
function negativeClassesTest(x: A | B) {
if ("a" in x) {
x.b = "1";
~
!!! error TS2339: Property 'b' does not exist on type 'A'.
} else {
x.a = "1";
~
!!! error TS2339: Property 'a' does not exist on type 'B'.
}
} The only problem i can see right now is dead code detection in following code: class AWithMethod {
a(): string { return "" }
}
class BWithMethod {
b(): string { return "" }
}
function negativeTestClassesWithMemberMissingInBothClasses(x: AWithMethod | BWithMethod) {
if ("c" in x) {
x.a();
~
!!! error TS2339: Property 'a' does not exist on type 'never'.
x.b();
~
!!! error TS2339: Property 'b' does not exist on type 'never'.
} else {
x.a();
~
!!! error TS2339: Property 'a' does not exist on type 'AWithMethod | BWithMethod'.
!!! error TS2339: Property 'a' does not exist on type 'BWithMethod'.
x.b();
~
!!! error TS2339: Property 'b' does not exist on type 'AWithMethod | BWithMethod'.
!!! error TS2339: Property 'b' does not exist on type 'AWithMethod'.
}
} The problem is the unreachable code detection occurs in binding phase and in the moment we do narrowing its already over |
@IdeaHunter yes, we're accepting PRs for this. Acceptance criteria would be a working feature with appropriate tests. The behavior you have there seems reasonably good - perhaps the author misspelled "c", after all ? We can run it on our internal test suite of partner code and see what the real-world breaks look like. |
I love Typescript and I'm glad this is open source work, but I think, at least as evidenced by this issue, the interaction between the maintainers and the community is lacking. This is an issue that was marked as "Accepting PRs" and @IdeaHunter spent what I'm sure was considerable effort to submit a 1400+ LOC PR, but it has been sitting ignored for going on 3 months. This has got to be off-putting to those in the community who are capable and willing to contribute. |
@EliSnow to be precise this PR have As you can see, I did nothing more that just enable existing code to handle new use case and put some unit tests to check that the results are sane IMHO i believe they on tight schedule working on plugin system design (which Im personally love to see released ahead of my PR) |
@IdeaHunter, I'm sure they are working on some pretty awesome features, many of which I'm looking forward to with excitement. I am not trying to be grossly critical, my only goal is to provide some constructive criticism in hopes that interaction between maintainers and the community can be improved. The maintainers have pretty strict rules in place for what and how the community can contribute, and that's fine, but IMHO when there are issues specifically marked for the community to work on, I think its a disservice to allow their contributions to sit ignored for long periods of time. |
Good job @IdeaHunter! 👍 |
Bueller? |
@pelotom Can you be more specific? |
@RyanCavanaugh this issue was marked as “help wanted”, and a PR was submitted 6 months ago, which has received no reaction from the TS team. Are there any plans to review and move forward with this? |
@pelotom reviewed. Sorry for the delay |
@RyanCavanaugh thank you! 😄 |
Thanks @IdeaHunter for implementing this! |
Added to roadmap https://github.com/Microsoft/TypeScript/wiki/Roadmap |
Cool, thank you. |
Inspired by #10421
The
in
operator could trivially be seen as a type guard:Basically, for a
n in x
wheren
is a string literal or string literal type andx
is a union type, the "true" arm narrows to types which have an optional or required propertyn
, and the "false" arm narrows to types which have an optional or missing propertyn
.The text was updated successfully, but these errors were encountered: