0

If i have a union type, how can i structure control flow to operate on the value as the actual concrete type that it contains? Sometimes I can do instanceof testing at runtime and then cast it, but that seems both clunky, unsafe, and not always possible (You can't instanceof a typed array). How can i operate on values safely when the actual type is a union?

In this example below, food.value causes a type error. Is there any way to make the compiler understand this is a {value:string} type without explicit casting?

type FoodSet = (string | { value: string })[]

const myValues : FoodSet = [
    "hamburger",
    "cheeseburger",
    {value: "salad"},
    "fish",
    {value: "quinoa"}
]

myValues.forEach(food => {
    if (food instanceof String) {
        console.log(food)
    } else {
        console.log(food.value) // <-- error here
    }
})

I'd love something like

switch(typeof food) {
    case string:
        console.log(food)
    case {value:string}:
        console.log(food.value)
}
Shaun Luttin
  • 133,272
  • 81
  • 405
  • 467
msfeldstein
  • 1,080
  • 1
  • 9
  • 18

3 Answers3

3

Your initial attempt is very close! The problem is that food instanceof String isn't quite right: that would work if FoodSet were (String | { value: string })[] (note the capital S).

To check if food is a string, you should use typeof food === "string" instead. That will correctly narrow the type in the else branch to { value: string }:

type FoodSet = (string | { value: string })[]

const myValues : FoodSet = [
    "hamburger",
    "cheeseburger",
    {value: "salad"},
    "fish",
    {value: "quinoa"}
]

myValues.forEach(food => {
    if (typeof food === "string") {
        console.log(food)
    } else {
        console.log(food.value)
    }
})
Mattias Buelens
  • 19,609
  • 4
  • 45
  • 51
  • interesting that made the compiler errors go away, but it also fails, due to what @artem said below, since the primitive strings aren't actually 'String' types. – msfeldstein Jun 14 '19 at 22:56
1

Javascsript is weird, and TypeScript, by design, does not even try to do anything to improve it at runtime. In Javascript, there are two kind of string values - primitive values and objects (or boxed strings).

So you have to check for both, or settle on using only one kind of string throughout the code. Usual recommendation is to avoid using String class at all. You can see the difference in javascript console:

let primitiveString = 'a';
> 
typeof primitiveString === 'string'
> true  // note that the result of runtime `typeof` is a string
primitiveString instanceof String
> false // primitiveString was not created with the String() constructor, of course
let objectString = new String('a');
>
typeof objectString === 'string'
> false   // typeof objectString returns 'object'
primitiveString instanceof String
> true  
objectString === primitiveString
> false
objectString == primitiveString
> true

I'd love something like

switch(typeof food) {
    case string:
        console.log(food)
    case {value:string}:
        console.log(food.value)
}

There are two different typeof operators in TypeScript.

One is compile-time only, it allows to refer to a type of some variable or function.

Another one is run-time typeof which is the same as in javascript, and it returns very limited amount of information because TypeScript does not generate any kind of type information available at runtime.

So, every time you want to check a type of the object at runtime, you need to write a series of if statements with different kind of checks, as appropriate for the situation:

  • typeof if you are checking for the kind of built-in type like string, number, booelan, function or object (object basically means "anything else")

  • instanceof if you want to know which constructor was used to create an object (note that if the value was created as object literal, it will be instance of Object, however it may well be compatible with some class because objects literals can have methods too)

  • in operator if you want to check if an object has some property or method

  • Array.isArray() to check if something is an array

and so on.

The compiler detects an error in your code

myValues.forEach(food => {
    if (food instanceof String) {
        console.log(food)
    } else {
        console.log(food.value) // <-- error here
    }
})

because instanceof String will be false for primitive strings, so the false branch will be executed trying to access value property which does not exists on strings. The usual way to check for a string type is

 if (typeof food === 'string') {

as explained in another answer.

artem
  • 46,476
  • 8
  • 74
  • 78
  • wow that primitive vs typed String is incredibly unfortunate. Changing my instanceof to typeof makes the code work, but also makes the compiler fail again in the same way. – msfeldstein Jun 14 '19 at 22:57
1

https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards

User defined type guards is what i was looking for


interface Bird {
    fly();
    layEggs();
}

interface Fish {
    swim();
    layEggs();
}


function isFish(pet: Fish | Bird): pet is Fish {
    return (<Fish>pet).swim !== undefined;
}

if (isFish(pet)) {
    pet.swim();
}
else {
    pet.fly();
}
msfeldstein
  • 1,080
  • 1
  • 9
  • 18