Skip to content

Make property access type checking less restictive #39065

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

Closed
4 of 5 tasks
PetaSoft opened this issue Jun 14, 2020 · 8 comments
Closed
4 of 5 tasks

Make property access type checking less restictive #39065

PetaSoft opened this issue Jun 14, 2020 · 8 comments
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript

Comments

@PetaSoft
Copy link

PetaSoft commented Jun 14, 2020

Search Terms

TypeScript 3.9, property access, type checking, union types

Suggestion

I want to propose a less restictive way of type checking for property access. To illustrate the new way of type checking, I want to cite the example which is mentioned in the TypeScript Handbook (section "Advanced Types", subsection Type Guards and Differentiating Types):

interface Bird { 
  fly(): void; 
  layEggs(): void; 
} 
 
interface Fish { 
  swim(): void; 
  layEggs(): void; 
}
 
declare function getSmallPet(): Fish | Bird;

let pet = getSmallPet();

// Each of these property accesses will cause an error 
if (pet.swim) {
  pet.swim();
} else if (pet.fly) {
  pet.fly();
}

The property access pet.swim is not allowed because type Bird does not have a property swim. In Javascript these kinds of property access are allowed and if pet does not have that property the result value is undefined.
I think that a property access pet.swim should be allowed even if not all parts of the union type have that property. The type of the result would be () => void | undefined. The if(pet.swim) type guard would eleminte the undefined from the result type in the body of the if statement, and pet.swim() would be allowed, too. The same would be the case for if(pet.fly).
This less restictive type checking does not cause any runtime errors, and more Javascript code can be ported to Typescript without any change.
I want to mention that of course a statement pet.swim() without a type guard would still cause a compile time error as pet.swim is of type () => void | undefined and undefined is not callable.

Going one step further, it can also make sense to allow property access even if the type of the object does not have a member with that property name:

declare let fooBird: Bird;
let x = fooBird.swim;    // x is of type undefined

Of cource, once again x() is not allowed as undefined is not callable. These kinds of property access do not cause any runtime errors but the first design goal of TypeScript is probably too much affected if it would be allowed.
At least, a compiler option would be helpful which allows such less restictive property accesses. Additionally, the compiler could emit warnings for such property accesses instead of errors.

I want to mention that the solution presented in the TypeScript Handbook does no longer work because of stricter type checks in if statements:

let pet = getSmallPet();
if ((pet as Fish).swim) {     // error
  (pet as Fish).swim();
} else if ((pet as Bird).fly) {   // error
  (pet as Bird).fly();
}

One gets the error: "This condition will always return true since the function is always defined".
It can be fixed using the in operator:

if ('swim' in pet) {
  pet.swim();
} else if ('fly' in pet) { 
  pet.fly();
}

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@MartinJohns
Copy link
Contributor

Duplicate of #38842 (and others).

@PetaSoft
Copy link
Author

@MartinJohns Thank You once again for the hint, but this time it is not really a duplicate though the topic is similar. You mentioned a bug report but I know that the current behaviour of TypeScript is not a bug but a feature. I opened a feature request to make a different behaviour possible. It can be an optional feature realized by a compiler option, or a complete change of the behaviour.
Maybe, the TypeScript team does not want that change because of the design goals of TypeScript but the decision lays at the TypeScript team.

@RyanCavanaugh RyanCavanaugh added Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript labels Jun 14, 2020
@RyanCavanaugh
Copy link
Member

The "lack" of this feature is very much intentional; this pattern is not safe to use because object types aren't closed, so the existence of a value in some property slot is really not a solid indication that anything correct is happening. The in operator is granted an exception to this because there's nothing else you might be intending to do with the in operator.

See e.g. #37343 (comment)

@PetaSoft
Copy link
Author

@RyanCavanaugh Maybe I went too far with my suggestion but isn't it okay to allow it at least in an if statement in the case that at least one of the types of the union has such a property and the value of that property is always a function for all types of the union that have that property?

@RyanCavanaugh
Copy link
Member

@PetaSoft no. You could write something like this, for example, which is legal according to the type system but would crash at runtime:

interface Bird { 
  fly(): void; 
  layEggs(): void; 
} 
 
interface Fish { 
  swim(): void; 
  layEggs(): void; 
}
 
function getSmallPet(): Fish | Bird {
  const b = { swim: true, fly() { }, layEggs() { } };
  return b;
}

let pet = getSmallPet();

// Each of these property accesses will cause an error 
if (pet.swim) {
  pet.swim();
} else if (pet.fly) {
  pet.fly();
}

@PetaSoft
Copy link
Author

@RyanCavanaugh Your example should be added to the TypeScript Handbuch (subsection Type Guards and Differentiating Types). It is a very good reason not to allow a property access in an if(pet.swim). I was not aware of the fact, that a Bird can have a swim property which is different from a function/method though I know that TypeScript uses a structural type system. I simply did not have the idea.
I am sorry that I wasted Your time, and I will now close this issue.

@RyanCavanaugh
Copy link
Member

No worries 🙂

@gkiely
Copy link

gkiely commented Oct 26, 2021

@RyanCavanaugh In your example, why doesn't the type checker catch the run time error?

It appears to know it's a boolean at the return type.
image

This issue around property access seems like it has been requested a lot. I'm surprised there hasn't been a fix besides asking all devs to switch to using in.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

4 participants