Skip to content

Typecast sometimes warns about an error when properties don't align #5853

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
denis-sokolov opened this issue Dec 1, 2015 · 10 comments
Closed
Labels
Canonical This issue contains a lengthy and complete description of a particular problem, solution, or design Question An issue which isn't directly actionable in code

Comments

@denis-sokolov
Copy link

Some inconsistent behavior in errors on type casts.
In the following example, I expected consistency - either x and y is an error, or none of them are.

// No error
const x = { a: 'a' } as { a: string, b: string }
// Error
const y = { a: 'a', c: 'c' } as { a: string, b: string }
// No error
const z = { a: 'a', c: 'c' } as { a: string }

// Always an error, as expected
const f = (p: {a: string, b: string}) => 0
f({ a: 'a' })
f({ a: 'a', c: 'c' })

Playground

I do not understand how an extra parameter on y changes the behavior, seems a bug to me:

@kitsonk
Copy link
Contributor

kitsonk commented Dec 1, 2015

I think the answer is that the value of x is a compatible shape with its type... x.b = undefined would be a valid type structure. Where as y.c is a different structure that isn't compatible with { a: string, b: string }.

As of 1.6 and strict object literal checking, you cannot have excess properties... y.c is an excess property for an object literal. Prior to 1.6, y would not be an error.

#185 covers the non-nullable type, which would cause x to error as well, because it seems a bit counter-intuitive, that something undefined actually matches the structure in that case.

@denis-sokolov
Copy link
Author

@kitsonk, that is a very reasonable hypothesis. Sorry I forgot to include a case which possibly disproves it:

// No error
const z = { a: 'a', c: 'c' } as { a: string }

@kitsonk
Copy link
Contributor

kitsonk commented Dec 1, 2015

Ok, I give up! 😄

const z: { a: string } = { a: 'a', c: 'c' }; // Errors

¯\_(ツ)_/¯

@denis-sokolov
Copy link
Author

I do not understand TypeScript very well, but it is my understanding that as is supposed to silence the type-checked and trust the developer. In that case, only the y being an error is a bug. The rest is consistent with the hypothesis.

@mhegazy
Copy link
Contributor

mhegazy commented Dec 1, 2015

Type assertions, either using the postfix as operator or using the prefix style <T> are a "assert" to the compiler the type of an expression. The rule is that ether the type of the expression is assignable to the type (down casting) or the type is assignable to the type of the expression (up casting). this is why things like:

// not an error, because { a: string, b: string } is assignable to { a: string } 
const x = { a: 'a' } as { a: string, b: string} 

 // error, because neither { a: string, c: string } is assignable to { a: string, b: string } nor  { a: string, b: string } to { a: string, c: string }
const y = { a: 'a', c: 'c' } as { a: string, b: string }

// not an error, as { a: string, c: string } is assignable to { a: string } 
const z = { a: 'a', c: 'c' } as { a: string }; 

You can find more information about type assertions in: https://github.com./Microsoft/TypeScript/blob/master/doc/spec.md#416-type-assertions

Now, this was intentionally lose, to allow you to up cast and down cast. but normal assignments are checked in a more strict fashion.

var ab : {a: string, b: string};

ab = { a: "a" }; // Error missing b
ab = { b: "b" }; // Error missing a
ab = { a: "a", b: "b" }; // OK

One observation here is that object literals assignment does not fail if the target has optional properties that were not specified in the target, e.g.:

var maybeC : {c? : string};

maybeC = { cc: "intentional?" }; 

as per the basic algorithm this is fine, as the target maybeC allows missing the property c. in practice this has been a source of errors, usually you would misspell an optional property name, the compiler will not complain, and your program will not do what you expected it will. so in 1.6 we introduced the "excess property checks" on object literals, which will error if there are properties in the source that were not specfiied in the target, so cc above will be flagged as an error.

hope that helped.

@mhegazy mhegazy added the Question An issue which isn't directly actionable in code label Dec 1, 2015
@denis-sokolov
Copy link
Author

Okay, I understand that this is the designed functionality, and thus, not a bug. Thank you for a very thorough explanation in terms of the implementation.

If it's not too impolite, may I ask you for explanations in terms of use cases?
I can see a use case for const z = { a: 'a', c: 'c' } as { a: string };. This is popular duck typing - I have something that does a, it does not matter that it also does c.

But what about the opposite? In const x = { a: 'a' } as { a: string, b: string} I am trying to express that I have something that I do not. Moreover, whichever this use case is, I cannot see how that use case would not cover const y = { a: 'a', c: 'c' } as { a: string, b: string }.

@RyanCavanaugh
Copy link
Member

Fair questions. Let's think about some concrete examples that have the same type relationships as the types in your examples

const x = { a: 'a' } as { a: string, b: string}

This example might be rewritten as

const myTextbox = document.getElementById('myInput') as HTMLInputElement;

Here we got back an HTMLElement, but have the additional ouf-of-band information that it's actually an HTMLInputElement. This is a very plausible assertion -- often we have something of type Animal but we know it's actually a Dog for whatever reason. This is allowed even though HTMLInputElement has properties that HTMLElement doesn't have. I should note that "X has properties Y doesn't have" should be very unsurprising as you would almost never use a type assertion if the type you were asserting to wasn't more specific (otherwise there would usually not be a reason to assert in the first place).

Now this example:

const y = { a: 'a', c: 'c' } as { a: string, b: string }.

We might rewrite this as:

const myTextbox = document.getElementById as HTMLInputElement;

This code is not good. We forgot to invoke the function. You might say "But in my example, the types had a property in common", but this is also true of the types here -- both the function and the HTMLInputElement have properties like toString in common. Nearly any two types will have at least one property in common (things like name and length and count appear all over the place).

The point with this example is that not all type assertions should be allowed. The assertion needs to be at least somewhat reasonable. If you really, really know what's going on, you can chain together two type assertions to work around this, but I have never seen a case where that is required when the definition for the type is actually correct.

@RyanCavanaugh RyanCavanaugh added the Canonical This issue contains a lengthy and complete description of a particular problem, solution, or design label Dec 1, 2015
@denis-sokolov
Copy link
Author

This has been very helpful, @RyanCavanaugh. It still seems partially inconsistent theoretically, but the practical benefits are very reasonable.

It is notable that your explanation is very inheritance oriented. What about the interface-heavy workflow?
The same example with the input element may be presented as follows:

interface AttachedToDom { getDomTree: Function }
interface ValueContaining { getValue: () => string }
const myTextbox = getAttachedDomElement('myInput') as ValueContaining
// getAttachedDomElement returns AttachedToDom

Pardon my poor attempt at coming up with proper interfaces, I hope you see the point where the interface-based workflow is far less likely to have the cast be strictly up or down.
If I understand correctly, your approach to this is 'tough luck', right?
Not judging at all, only trying to fully understand.

P.S. It seems this mini-essay of your would be very valuable, if added to the TypeScript Handbook.

@RyanCavanaugh
Copy link
Member

FWIW I use inheritance-based examples because the subtype/supertype relations are more clear.

It seems better to write ... as ValueContaining & AttachedToDom in that example, which would typecheck without error and is higher-fidelity. Alternatively, if getAttachedDomElement is going to return arbitrary objects from which you might select a trait of your own choice, having it return {} instead of AttachedToDom might be better.

@denis-sokolov
Copy link
Author

These are two great suggestions. Thank you a lot for your help.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Canonical This issue contains a lengthy and complete description of a particular problem, solution, or design Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

4 participants