Why does the Typescript compiler complain about the following code?
type Foo = {
a: string
}
type Bar = {
b: number
}
type Baz = Foo | Bar;
function f(x: Baz): number {
if (x.a) { // property 'a' does not exist on type Bar!
return 0;
}
if (x.b) { // property 'b' does not exist on type Foo!
return 1;
}
return -1;
}
Link to Playground
Use the typeof operator to check the type of a variable in TypeScript, e.g. if (typeof myVar === 'string') {} . The typeof operator returns a string that indicates the type of the value and can be used as a type guard in TypeScript.
What does ?: mean in TypeScript? Using a question mark followed by a colon ( ?: ) means a property is optional. That said, a property can either have a value based on the type defined or its value can be undefined .
TypeScript automatically finds type definitions under node_modules/@types , so there's no other step needed to get these types available in your program.
Summary. The TypeScript any type allows you to store a value of any type. It instructs the compiler to skip type checking. Use the any type to store a value that you don't actually know its type at the compile-time or when you migrate a JavaScript project over to a TypeScript project.
Consider the following cases mentioned on this github thread linked in the comments by jcalz:
interface Vec2 {
x: number
y: number
}
interface Vec3 {
x: number
y: number
z: number
}
const m = { x: 0, y: 0, z: "hello world" };
const n: Vec2 = m; // N.B. structurally m qualifies as Vec2!
function f(x: Vec2 | Vec3) {
if (x.z) return x.z.toFixed(2); // This fails if z is not a number!
}
f(n); // compiler must allow this call
Playground
Here the author of the code makes an unfortunate assumption that just because a property is present and truthy that it is a certain type. But this is doubly wrong: you could have a falsey value of the correct type (zero or NaN in this case) or a truthy value of a different type. But there are subtler gotchas:
type Message =
{ kind: "close" } |
{ kind: "data", payload: object }
function handle(m: Message) {
switch (m.kind) {
case "close":
console.log("closing!");
// forgot 'break;' here
case "data":
updateBankAccount(m.payload);
}
}
This is a scenario where you'd want the compiler to complain about an unintended property access, not just silently propagate undefined
. Catching this sort of thing is a big part of why we use static analysis in the first place.
The Typescript compiler is already a marvelous feat of engineering layering a static type system on top of not just a dynamic language but an ultra-dynamic language. What you're looking for here is called type narrowing, where you take a value that could possibly be more than one type and then narrow it down to a specific type. The TS compiler supports (at least) five different idioms to achieve this:
instanceof
operator.typeof
operator.in
operator.Let's look at each in turn:
This one works well for user-defined classes:
class A {
public a: number
constructor () {
this.a = 4;
}
}
class B {
public b: number
constructor () {
this.b = 5;
}
}
type AB = A | B;
function abba(x: AB): number {
if (x instanceof A) return x.a;
if (x instanceof B) return x.b;
return 0;
}
Playground
This one works well for JS primitives (undefined, numbers, strings, booleans, etc).
type snumber = string | number;
function f(x: snumber): string {
if (typeof x === 'number') {
return x.toFixed(2); // strings don't have toFixed
} else {
return x.repeat(2); // numbers don't have repeat
}
}
Playground
This one works well for structurally typed objects:
type A = {
a: number
}
type B = {
b: string
}
type AB = A | B;
function f(x: AB): number {
if ('a' in x) return x.a;
if ('b' in x) return 5;
return 0;
}
Playground
An astute reader will notice that this has the same problems as the first motivating example above, namely that the existence of a property on an object does not in any way guarantee the type. This was a pragmatic decision on the part of the TS team to allow an uncommon behavior for a simple opt-in idiom of wanting to get either the value or undefined
, and much like a cast is an implicit promise that the programmer is taking responsibility for the possible outcome.
This works well for just about anything, but is more verbose than the earlier options. This one is straight from the TS Handbook:
function isFish(pet: Fish | Bird): pet is Fish { // note the 'is'
return (pet as Fish).swim !== undefined;
}
let pet = getSmallPet();
if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}
This works best when you have a bunch of very similar objects that differ only in the (statically knowable!) value of a single property:
type A = {
a: string
kind: 'is-an-a'
}
type B = {
b: number
kind: 'is-a-b'
}
type AB = A | B;
function f(x: AB): string {
switch (x.kind) {
case 'is-an-a': return x.a;
case 'is-a-b': return '' + x.b;
}
}
Note that you will as I said need to make the discriminant (the kind
property in this case) a statically knowable value, usually a string literal or a member of an enum. You can't use variables, because their values aren't known at compile-time.
Playground
So in summary the Typescript compiler can figure it out, you just have to use an idiom it can statically verify instead of one that it can't, and it gives you a fair number of options.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With