Reputation: 9427
Say I have three base classes or types as below:
type A = { one: number };
type B = { one: number, two: string };
type C = { one: number, two: string, three: boolean };
Now I would like to create a typed array that holds a number any of instances of these types, so I declare a Union Type which will be used for the array:
type X = A | B | C;
Defining the array elements here and constructing the array:
const a: A = { one: 1 };
const b: B = { one: 1, two: '2' };
const c: C = { one: 1, two: '2', three: true };
let list: X[] = [a, b, c];
Now if I try to access the two
property of the second or third element in this array, I get the following error:
const z: X = list[2];
z.two; // yields this error
error TS2339: Property 'two' does not exist on type 'X'. Property 'two' does not exist on type 'A'.
I have tried changing the type of A
-- and others in fact, from const a: A
to const a: X
, but I still get the same error.
If I cast z: X
to z: B
instead; I still get a very similar error again:
const z: B = list[2];
z.two; // yields this error
Type 'X' is not assignable to type 'B'. Type 'A' is not assignable to type 'B'. Property 'two' is missing in type 'A'.
To me it does seem that TypeScript's type inference mechanism is somehow overrides my explicit typing and basically instead of creating list: X[]
it has created a down-casted version which is list: A[]
.
I have also tried using class
and interface
definition to see if it makes a difference -- even though I was sure it wouldn't due to TypeScript's Structural Type System, and as expected nothing changed.
Any idea what I'm doing wrong here or any suggestions for a better approach?
Answer
It turned-out change const z: B = list[2]
to const z = <B>list[2]
does the proper casting that I had the intention of.
Upvotes: 0
Views: 1821
Reputation: 330571
It's not overriding your explicit typing... it's obeying your explicit typing by treating list
as an array of elements of type X
. An X
can be either an A
, a B
, or a C
. If I hand you a value of type X
, it is safe to read the one
property, because that definitely exists. But it is unsafe to try to read the two
property, because the X
might be an A
, and A
is not known to have a two
. So you get a useful error:
z.two; // error!
// Property 'two' does not exist on type 'X'. Property 'two' does not exist on type 'A'.
So, what are your options? One is to just tell the compiler that you know better than it does, by using a type assertion as in the other answer:
(z as B).two; // okay now
This suppresses the compiler error, but that really isn't a great solution because it is partially disabling type checking when it doesn't need to. The following also will not be an error, but it would give you problems at runtime:
(list[0] as B).two.length; // no compile error
// at runtime: TypeError: list[0].two is undefined
Usually, type assertions should be a last resort of dealing with compiler errors, to be used only where you cannot find a reasonable way to convince the compiler that what you are doing is safe, and you are positive that it is safe and will remain safe even in the face of possible code changes (e.g., you change list
to [b,c,a]
in the future).
A better solution is to use type guards to convince the compiler that what you are doing is safe. This has a run-time impact, in that you are running more code, but the code you are running is more future-proof if you change list
somewhere down the line. Here's a way to do it:
const z = list[2]; // z is inferred as A | B | C
if ('two' in z) {
// z is now narrowed to B | C
z.two; // okay
}
So you are guarding the read of z.two
by using in
to check the presence of the two
property before using it. This is now safe. If you are thinking "why should I go through this trouble when I know that z
will be of type B
(actually C
, ha ha, list[2]
is the third element)", then read on:
If you are sure that list
will always be a three-element array of types A
, B
, and C
, in that order, then you can tell the compiler this and get the behavior you expect at compile time without any runtime guarding. You are looking for tuple types:
const list: [A, B, C] = [a, b, c];
const z = list[2];
z.two; // okay
z.three; // okay
You've told the compiler that list
is a three-element tuple of type [A, B, C]
. Now there are no errors (and it can see that z
is a C
). This is safe, with zero run-time impact. It also prevents you from messing with list
:
list[1] = a; // error! A is not assignable to B
since you've told it list[1]
is always a B
, then you must assign something compatible with B
to it:
list[1] = c; // okay, C is assignable to B
So there are three options for you: assertions, type guards, and tuples, with tuples being the one I'd recommend for this situation. Hope that helps. Good luck!
Upvotes: 4
Reputation: 98
Have you tried using a type assertion eg.
const z = <B>list[2]; // omitting the .two because that would never be assignable to B
?
Upvotes: 1