type p1 = { a: number, b: string }
type p3 = { a: string }
type p4 = p1 | p3
let demo: p4 = { a: '123', b: '123' }
function isP3(obj: p4): obj is p3 {
return typeof (<p3>obj).a === 'string'
}
function func(obj: p4) {
if ('b' in obj) {
// Uncaught TypeError: obj.a.toFixed is not a function
obj.a.toFixed() //<- Now, no error is given
} else {
}
}
func(demo)
Why the demo did not report an error when initialize? User-Defined Type Guards
This is an open issue (microsoft/TypeScript#20863) in TypeScript. Your union type is not a discriminated union , so the compiler doesn't split the union up into members before performing excess property checking . Most people (myself included) would expect that excess property checks should occur for each member of the union whether or not the union is a discriminated one. For now, though, that's just the way it is: the compiler sees that "b"
is an acceptable property in at least one of the union members and decides not to complain.
Note that excess property checking is a convenience, and not a matter of type safety. Object types in TypeScript are open , and you can always add more properties to them over what's in the definition without violating the type. A value {x: 1, y: 2}
is a valid {x: number}
despite having that y
property. Another way of saying this is that object types in TypeScript are not exact . It is therefore technically true that { a: '123', b: '123' }
is a valid p3
and therefore a valid p4
. And so, technically, you can't just check for the presence or absence of b
to distinguish between p1
and p3
. Yes, if you just try to say const demo: p3 = {a: '123', b: '123'}
you'll get an excess property warning on "b"
, but this is, as I said, just a convenience. It is easily defeated:
const demo1 = { a: '123', b: '123' };
const demo2: p3 = demo1; // no error
At this point you might be wondering: "wait, if "b"
does not properly distinguish p1
from p3
, why does the compiler think it does inside func()
?". Good question:
if ('b' in obj) { // why does the compiler think this narrows obj to p1?
obj.a.toFixed() // no error, but blows up at runtime
}
Well, it turns out that the in
type guard is intentionally unsound . It's technically not safe to use it, but people do and usually it's not a problem. But it doesn't help you here. Oh well.
So, what should you do here? If your intent is to make a test for b
distinguish between p1
and p3
, then your p3
type should make that clear:
type p3 = { a: string, b?: undefined }; // p3 cannot have a defined "b" property
Now the p4
type is, as of TypeScript 3.2+ , a true discriminated union. And so this is an error:
let demo: p4 = { a: '123', b: '123' } // error now
And makes the unsound 'b'
test show up as an error. If you want to do a "good" b
test, you can now test for obj.b !== undefined
, which will definitely distinguish between a p1
and p3
with the new p3
definition:
function func(obj: p4) {
if ('b' in obj) {
obj.a.toFixed() // error now
}
if (obj.b !== undefined) {
obj.a.toFixed(); // okay
}
}
Okay, hope that helps; good luck!
Use your custom type guard function to narrow types down instead of 'b' in obj
if (!isP3(obj)) {
obj.a.toFixed() // Error
} else {
}
}
About the assignment let demo: p4 = { a: '123', b: '123' }
it bothers me too that no error is given. As I found out it will work properly (which means giving an error) if we define a
as a boolean instead of a number. Looks like the assignment fails only if the discriminator type contains union type itself. You can subscribe to this issue for details https://github.com/microsoft/TypeScript/issues/35861
Section of the spec on the matter doesn't explain fully the current behavior. Looks like for an explanation one have to peek into the compiler itself. https://github.com/Microsoft/TypeScript/blob/master/doc/spec.md#34-union-types
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.