Need to type guard but instanceof
doesn't work with TypeScript type
s:
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);
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:
- the expression context (the parts to be emitted later as JavaScript)
- 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.