211

I have a custom type, let's say

export type Fruit = "apple" | "banana" | "grape";

I would like to determine if a string is part of the Fruit type. How can I accomplish this?

The following doesn't work.

let myfruit = "pear";
if (typeof myfruit === "Fruit") {
    console.log("My fruit is of type 'Fruit'");
}

Any thoughts appreciated!

maia
  • 3,910
  • 4
  • 27
  • 34

3 Answers3

350

Short answer:

You can't use typeof at runtime to check for interface types, which only exist at compile time. Instead you can write a user-defined type guard function to check for such types:

const fruit = ["apple", "banana", "grape"] as const;
type Fruit = (typeof fruit)[number];
const isFruit = (x: any): x is Fruit => fruit.includes(x);

let myfruit = "pear";
if (isFruit(myfruit)) {
  console.log("My fruit is of type 'Fruit'");
}

Long answer follows:


You might be confused about the difference between values and types in TypeScript, especially as it relates to the typeof operator. As you may be aware, TypeScript adds a static type system to JavaScript, and that type system gets erased when the code is transpiled. The syntax of TypeScript is such that some expressions and statements refer to values that exist at runtime, while other expressions and statements refer to types that exist only at design/compile time. Values have types, but they are not types themselves. Importantly, there are some places in the code where the compiler will expect a value and interpret the expression it finds as a value if possible, and other places where the compiler will expect a type and interpret the expression it finds as a type if possible.

The typeof operator leads a double life. The expression typeof x always expects x to be a value, but typeof x itself could be a value or type depending on the context:

let bar = {a: 0};
let TypeofBar = typeof bar; // the value "object"
type TypeofBar = typeof bar; // the type {a: number}

The line let TypeofBar = typeof bar; will make it through to the JavaScript, and it will use the JavaScript typeof operator at runtime and produce a string. But type TypeofBar = typeof bar; is erased, and it is using the TypeScript type query operator to examine the static type that TypeScript has assigned to the value named bar.

In your code,

let myfruit = "pear";
if (typeof myfruit === "Fruit") { // "string" === "Fruit" ?!
    console.log("My fruit is of type 'Fruit'");
}

typeof myfruit is a value, not a type. So it's the JavaScript typeof operator, not the TypeScript type query operator. It will always return the value "string"; it will never be Fruit or "Fruit". You can't get the results of the TypeScript type query operator at runtime, because the type system is erased at runtime. You need to give up on the typeof operator.


What you can do is check the value of myfruit against the three known Fruit string literals... like, for example, this:

let myfruit = "pear";
if (myfruit === "apple" || myfruit === "banana" || myfruit === "grape") {
  console.log("My fruit is of type 'Fruit'");
}

Perfect, right? Okay, maybe that seems like a lot of redundant code. Here's a less redundant way to do it. First of all, define your Fruit type in terms of an existing array of literal values... TypeScript can infer types from values, but you can't generate values from types.

const fruit = ["apple", "banana", "grape"] as const;
export type Fruit = (typeof fruit)[number];

You can verify that Fruit is the same type as you defined yourself manually. Then, for the type test, you can use a user-defined type guard like this:

const isFruit = (x: any): x is Fruit => fruit.includes(x);

isFruit() is a function which checks if its argument is found in the fruit array, and if so, narrows the type of its argument to Fruit. Let's see it work:

let myfruit = "pear";
if (isFruit(myfruit)) {
  console.log("My fruit is of type 'Fruit'");
}

That type guard also lets the compiler know that inside the "then" clause of the if statement, that myfruit is a Fruit. Imagine if you had a function that only accepts Fruit, and a value that may or may not be a Fruit:

declare function acceptFruit(f: Fruit): void;
const myfruit = Math.random() < 0.5 ? "pear" : "banana";

You can't call the function directly:

acceptFruit(myfruit); // error, myfruit might be "pear"

But you can call it inside the "then" clause after checking it:

if (isFruit(myfruit)) {
  acceptFruit(myfruit); // okay, myfruit is known to be "banana"
}

Which is presumably why you want to check against your custom type in the first place. So that lets you do it.


To recap: you can't use typeof. You can compare against strings. You can do some type inference and a type guard to eliminate duplicated code and get control flow type analysis from the compiler.

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • Thanks for explanation about getting type from array! Was wondering if it's really possible, just checked myself - worked perfectly. – Cerberus Jul 26 '18 at 02:44
  • 3
    Wow, fantastic answer, thank you! The only thing I had to change was `const isFruit = (x: any): x is Fruit => fruit.includes(x);` - instead of `fruit.includes(x)`, I had to write `fruit.indexOf(x) !== -1`. Otherwise, I received the following error: `Property 'includes' does not exist on type ...` – maia Jul 27 '18 at 15:24
  • Yeah, [`includes()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes) is an ES2016 feature and might not be present if you are transpiling down to ES5 or ES2015. – jcalz Jul 27 '18 at 15:26
  • 6
    This has to be one of the most interesting and informative S/O answers I have ever read. Thanks! – Methodician Aug 29 '19 at 23:14
  • 11
    It's indeed a great post. I however struggle to understand how this line type Fruit = (typeof fruit)[number]; is working – Scipion Jan 06 '20 at 12:49
  • 18
    `typeof fruit` is `Array<"apple" | "banana" | "grape">`, so `Fruit` is equivalent to `(Array<"apple" | "banana" | "grape">)[number]`. The syntax `T[K]` means: the type of the properties of `T` whose keys are of type `K`. So `(Array<"apple" | "banana" | "grape">)[number]` means "the types of the properties of `Array<"apple" | "banana" | "grape">` whose keys are of type `number`", or: "the array elements of `Array<"apple" | "banana" | "grape">`, or: `"apple" | "banana" | "grape"`. – jcalz Jan 06 '20 at 13:52
  • 4
    amazing explanation. is anything like this covered in the official docs? I'm especially amazed at this line: ````export type Fruit = (typeof fruit)[number]````. How can someone come up with that??? – Leaozinho Feb 23 '21 at 22:02
  • 1
    [Link to other @jcalz comment](https://stackoverflow.com/questions/45251664/typescript-derive-union-type-from-tuple-array-values#comment99063812_45257357) explaining how does `(typeof fruit)[number]` work. – OfirD Jul 15 '21 at 18:29
  • Indeed this is helping a lot of people, don't even joke about deleting it!! LOL Thank you so much!! Even knowing a bit about TS, these explanations are great to help us grasp the bits that are still foggy for some of us. :) – Vigil Sep 13 '22 at 06:29
  • One quibble with this solution: `Fruit` will type check as a regular string variable, so you won't be able to take advantage of type hinting, i.e your IDE won't suggest "grape" as a possible value and it won't complain if you say `fruit = 'broccoli';`. I guess this is a case choosing whether the explicit check is more important. – stakolee Apr 27 '23 at 14:31
8

typeof in TS:

The typeof operator in TS can be used in 2 different contexts:

  1. In an expression/value context to return a string of its type. This is just the JavaScript typeof operator and will remain after a compile.
  2. In a type context to make the type similar to an existing expression/value. This is a TS construct to help us express ourselves more easily with certain types. This will be compiled away and not be present in the compiled JavaScript

Examples:

Expression/value context

const hi = 'hi';
const one = 1;
const obj = {};

console.log(typeof hi, typeof 1, typeof obj);
// [LOG]: "string",  "number",  "object"

Type context:

const obj1 = {foo: 1, bar: true};
const obj2 = {foo: 1, bar: ''};

// test has the type according to the structure of obj1
const test: typeof obj1 = {foo: 1, bar: true};
// typeof obj1 is the same as:
type sameAsTypeofObj1 = {foo: number, bar: string}


// test2 has the type according to the structure of obj1
const test2: typeof obj2 = {foo: 1, bar: true};
// In test2 we get a compile error since bar is not correct
// Since the type of obj2 is {foo: number, bar: string} we get the error:
// Type 'boolean' is not assignable to type 'string'

For your specific problem I think you should use user defined type guards. Here is an example:

type Fruit = "apple" | "banana" | "grape";

let myfruit = "apple";

// user defined type guard
function isFruit(fruit: string): fruit is Fruit {
  return ["apple", "banana", "grape"].indexOf(fruit) !== -1;
}

if (isFruit(myfruit)) {
    // if this condition passes 
    // then TS compiler knows that myfruit is of the Fruit type
    console.log(`${myfruit} is a Fruit.`}
}
kburgie
  • 695
  • 6
  • 14
Willem van der Veen
  • 33,665
  • 16
  • 190
  • 155
-1

Generic type checking

If anyone else is looking for a generic version of a isFruit function, I was able to come-up with a check.

const isType = <Type>(thing: any): thing is Type => true;

This allows you to check different types without needing different type-checking functions.

if (isType<Fruit>(food)) {
  food.eat();
} else if (isType<Vegitable>(food)) {
  food.roast();
}
Douglas Meyer
  • 1,561
  • 12
  • 12
  • 2
    This will fail in practice because there is no runtime check whatsoever happening here. If you strip away the TypeScript, it boils down to `if (true) food.eat()`, regardless of whether food is a vegetable or not. – geisterfurz007 Jun 21 '23 at 14:46