Closed timcosta closed 3 years ago
as
will downcast (weirdRouteDefinition
) because this is what it's for, but it will not "side-cast" -- the expression type and the asserted type must be in some way related.
@RyanCavanaugh thanks for getting back to me! I think what's confusing is that adding an extra key to the object changes whether it errors due to a missing path or not. I would (personally) expect TS to consistently assert on the presence/absence of path
if the casting is consistently applied. I could understand if adding otherString
caused path
to stop erroring as the casting changed, but it's weird to me that it causes path
to start erroring when it otherwise wasnt.
If there are any docs that explain this feel free to point me that direction and tell me to RTFM - I just haven't found any that explain assertions becoming more strict as unknown keys are added.
Okay, so I still think there's weird behavior happening. I'm going to try to explain using phrases from the documentation that you've so kindly linked.
The basic rule for TypeScriptβs structural type system is that
x
is compatible withy
ify
has at least the same members asx
.
Great, this absolutely make sense. All members of x
must be present in y
for a type to be considered compatible. This lines up with the behavior seen below in my first two examples:
const workingRouteDefinition: ServerRoute = {
method: 'get',
path: '/'
};
const badRouteDefinition: ServerRoute = {
method: 'get',
};
workingRouteDefinition
contains every member of ServerRoute, so this instantiation passes type checking. badRouteDefinition
does not contain every member of ServerRoute, so this explodes during type checking.
Now we get to the cases that use type assertions rather than directly assigning types like the examples above do.
const weirdRouteDefinition = {
method: 'get',
} as ServerRoute;
This weirdRouteDefinition
unexpectedly passes the type assertion check. The docs from the type assertion section in v2 of the handbook states the following:
you can use a type assertion to specify a more specific type
TypeScript only allows type assertions which convert to a more specific or less specific version of a type
As far as I can tell, ServerRoute
is a more specific version of the inferred type object
, so this assertion is allowed by the compiler.
What doesn't make sense here is why a more specific type assertion is failing to error due to missing the path
key.
If we then look at the final example, we'll see that the more specific assertion starts working, but only after a key is added that isn't present in the more specific assertion.
const weirderRouteDefinition = {
method: 'get',
otherString: 'what'
} as ServerRoute;
The above snippet will start throwing errors about missing path
even though it seems like ServerRoute
is actually a less specific type assertion due to the additional key.
In both of these examples, the number of overlaps between the type asserted object and the type is the same single member. I'm not sure why TS is considering one of these objects to be missing a path
key while the other isn't missing the path
key when the number of overlapping members between the two objects and the type they are being asserting on is the same.
I dont see anything in the documentation that explains this behavior, I actually feel as though the documentation states the the behavior should be the inverse.
The first weird example should error due to a missing path
as the type being asserted against has more members than the object being checked which fails the "y
has at least the same members as x
" check from the type compatibility docs.
The second weirder example I gave should also error due to missing the path
key, like it is currently doing.
The following example I would expect to pass type checking, as it passes the type compatibility "y
has at least the same members as x
" check outlined in the docs, and it is a "less specific" type that is being asserted against.
const extendedRouteDefinition = {
method: 'get',
path: '/test',
otherString: 'what'
} as ServerRoute;
If this can't convince you that there's inconsistent behavior here when compared to the docs I'll stop pestering you, but everything you cited seems to state that the behavior I'm experiencing is unexpected due to type compatibility checking and the fact that in the problematic scenario I'm specifying a "more specific" version of a type and en error isn't being thrown for a missing key.
This behavior is 100% consistent with the definition of an allowable type assertion - that the expression type and the asserted type are comparable (which is the same as "assignable to or from" for the cases shown here). Every example here can be shown to be correctly following that rule.
You seem to expect that type assertions should not be capable of downcasting, but downcasting is the primary use case of type assertions, which I think indicates some fundamental misapprehensions about what's supposed to be happening here.
Thanks for the explanations Ryan. Much appreciated.
Bug Report
π Search Terms
object as interface not asserting properly
π Version & Regression Information
Typescript Version: 4.1.3
β― Playground Link
Playground link with relevant code
π» Code
Workbench Repro
This is primarily evident when doing something like
export default {} as Interface
in real code, but can happen when assigning consts as well in the above example case.π Actual behavior
Errors are not thrown where they should be.
π Expected behavior
Errors due to missing keys should always be present, not just when there's a third unknown key added. Unknown keys should have errors as well.