Home > Software design >  How to type guard TypeScript type if instanceof only supports classes?
How to type guard TypeScript type if instanceof only supports classes?

Time:11-19

Need to type guard but instanceof doesn't work with TypeScript types:

type Letter = 'A' | 'B';
const isLetter = (c: any): c is Letter => c instanceof Letter; // Error: 'Letter' only refers to a type, but is being used as a value here.

// Expected usage: Filter via type guard.
isLetter('a'); // Should output true
'foo bar'.split('').filter(c => isLetter(c)); // Should output 'a'

Haven't found similar questions but instanceof works with classes:

class Car {}
const isCar = (c: any): c is Car => c instanceof Car; // No error
isCar('a'); // false

If instanceof only works with classes, what is it's equivalent for type, & how can we type guard using a TypeScript type?

CodePudding user response:

TS types only exist in the compilation phase, not in runtime.

In a user-defined type guard it is your duty to implement appropriate checks. The check will vary depending on the type of input and output - selecting between a few alternatives is simpler than asserting the shape of completely unknown object.

For your type Letter (union of 'A' and 'B') it is enough to check whether input is A or input is B

const isLetter = (c: any): c is Letter => c == 'A' || c == 'B';

If your union has more members and you dont want to repeat them in union and in type guard:

const letters = ['A', 'B'] as const;
type Letter = typeof letters[number];
const isLetter = (c: any): c is Letter => letters.includes(c);

Playground link

Note: Classes are preserved at runtime (via prototype chain) - which is why you can use instanceof operator with classes.

CodePudding user response:

Found a possible but not ideal solution for type guarding a union strings type is to check against a list of the union strings:

const isLetter = (c: any): c is Letter => ['A', 'B'].includes(c);
isLetter('A'); // true
isLetter('a'); // false

Not ideal since the union strings are duplicated in the type & the type guard - open to better solutions

CodePudding user response:

Typescript is a compile-time technology. You are trying to use a Typescript type, which is a concept that only exists during compile time, to construct a runtime check (a check that is to be executed in the JavaScript runtime).

By the time the emitted JavaScript gets executed, the TypeScript notions are already long gone (that is, they are already stripped away).

Hence you should not try to implement this check in the body of the function as a runtime check, using the TypeScript interface/type. This will not work.

To implement a compile-time check, on the other hand, is just a matter of replacing any for the parameter type with the TypeScript type Letter.

This code might help clarify the matter:

type Letter = 'A' | 'B';
// compile-time check is implemented by assigning the parameter 'c'
// to a TypeScript type
const shouldBeLetter = (c: Letter) => {
  // implement a runtime check using JavaScript, not TypeScript
  if (c !== 'A' && c !== 'B') throw "c should be either 'A' or 'B'";
  /* rest of the function */
}

// example of a statament that fails at compile time
// error: Argument of type '"D"' is not assignable to parameter of 
//        type 'Letter'. (2345)
shouldBeLetter('C');

// example of a statement that won't fail at compile time, but will
// need a runtime test
let couldBeC : Letter = 'ABC'[Math.floor(Math.random() * 3)] as Letter;
shouldBeLetter(couldBeC);

PS: The provision of compile-time types in TypeScript, which are stripped away from the emitted (the compiled) JavaScript files, creates two distinct contexts within of a TypeScript code file:

  1. the expression context (the parts to be emitted later as JavaScript)
  2. the type context (the TypeScript parts which will be removed)

In the so-called, type context, it is possible to repurpose some of JavaScript's keywords to have a different meaning in TypeScript. An example is the typeof keyword: https://www.typescriptlang.org/docs/handbook/2/typeof-types.html

In JavaScript, typeof is a unary operator that returns a string at runtime, that string correlates with the type of the value/variable following it.

... when used in TypeScript, in the type context, it gives the TypeScript-equivalent type of the value/variable following it.

In the same way, expression-like notions such as letters[number] can be used in the type context in TypeScript to derive type information from actual variables, as explained here: https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html

Both points are very neatly utilized in the other answer by @Lesiak when reusing the enumerated-type array between the expression context and the type context within the code.

  • Related