I have a simple union type of string literals and need to check it\'s validity because of FFI calls to \"normal\" Javascript. Is there a way to ensure that a certain variabl
If you have several string union definitions in your program that you'd like to be able to check at runtime, you can use a generic StringUnion
function to generate their static types and type-checking methods together.
// TypeScript will infer a string union type from the literal values passed to
// this function. Without `extends string`, it would instead generalize them
// to the common string type.
export const StringUnion = <UnionType extends string>(...values: UnionType[]) => {
Object.freeze(values);
const valueSet: Set<string> = new Set(values);
const guard = (value: string): value is UnionType => {
return valueSet.has(value);
};
const check = (value: string): UnionType => {
if (!guard(value)) {
const actual = JSON.stringify(value);
const expected = values.map(s => JSON.stringify(s)).join(' | ');
throw new TypeError(`Value '${actual}' is not assignable to type '${expected}'.`);
}
return value;
};
const unionNamespace = {guard, check, values};
return Object.freeze(unionNamespace as typeof unionNamespace & {type: UnionType});
};
We also need a line of boilerplate to extract the generated type and merge its definition with its namespace object. If this definition is exported and imported into another module, they will get the merged definition automatically; consumers won't need to re-extract the type themselves.
const Race = StringUnion(
"orc",
"human",
"night elf",
"undead",
);
type Race = typeof Race.type;
At compile-time, the Race
type works the same as if we'd defined a string union normally with "orc" | "human" | "night elf" | "undead"
. We also have a .guard(...)
function that returns whether or not a value is a member of the union and may be used as a type guard, and a .check(...)
function that returns the passed value if it's valid or else throws a TypeError
.
let r: Race;
const zerg = "zerg";
// Compile-time error:
// error TS2322: Type '"zerg"' is not assignable to type '"orc" | "human" | "night elf" | "undead"'.
r = zerg;
// Run-time error:
// TypeError: Value '"zerg"' is not assignable to type '"orc" | "human" | "night elf" | "undead"'.
r = Race.check(zerg);
// Not executed:
if (Race.guard(zerg)) {
r = zerg;
}
This approach is based on the runtypes library, which provides similar functions for defining almost any type in TypeScript and getting run-time type checkers automatically. It would be a little more verbose for this specific case, but consider checking it out if you need something more flexible.
import {Union, Literal, Static} from 'runtypes';
const Race = Union(
Literal('orc'),
Literal('human'),
Literal('night elf'),
Literal('undead'),
);
type Race = Static<typeof Race>;
The example use would be the same as above.
using type
is just Type Aliasing and it will not be present in the compiled javascript code, because of that you can not really do:
MyStrings.isAssignable("A");
What you can do with it:
type MyStrings = "A" | "B" | "C";
let myString: MyStrings = getString();
switch (myString) {
case "A":
...
break;
case "B":
...
break;
case "C":
...
break;
default:
throw new Error("can only receive A, B or C")
}
As for you question about isAssignable
, you can:
function isAssignable(str: MyStrings): boolean {
return str === "A" || str === "B" || str === "C";
}
Since Typescript 2.1, you can do it the other way around with the keyof operator.
The idea is as follows. Since string literal type information isn't available in runtime, you will define a plain object with keys as your strings literals, and then make a type of the keys of that object.
As follows:
// Values of this dictionary are irrelevant
const myStrings = {
A: "",
B: ""
}
type MyStrings = keyof typeof myStrings;
isMyStrings(x: string): x is MyStrings {
return myStrings.hasOwnProperty(x);
}
const a: string = "A";
if(isMyStrings(a)){
// ... Use a as if it were typed MyString from assignment within this block: the TypeScript compiler trusts our duck typing!
}
You cannot call a method on a type, because types don't exist in runtime
MyStrings.isAssignable("A"); // Won't work — `MyStrings` is a string literal
Instead, create executable JavaScript code that will validate your input. It's programmer's responsibility to ensure the function does its job properly.
function isMyString(candidate: string): candidate is MyStrings {
return ["A", "B", "C"].includes(candidate);
}
Update
As suggested by @jtschoonhoven, we can create en exhaustive type guard that will check if any string is one of MyStrings
.
First, create a function called enumerate
that will make sure all members of the MyStrings
union are used. It should break when the union is expanded in the future, urging you to update the type guard.
type ValueOf<T> = T[keyof T];
type IncludesEvery<T, U extends T[]> =
T extends ValueOf<U>
? true
: false;
type WhenIncludesEvery<T, U extends T[]> =
IncludesEvery<T, U> extends true
? U
: never;
export const enumerate = <T>() =>
<U extends T[]>(...elements: WhenIncludesEvery<T, U>): U => elements;
The new-and-improved type guard:
function isMyString(candidate: string): candidate is MyStrings {
const valid = enumerate<MyStrings>()('A', 'B', 'C');
return valid.some(value => candidate === value);
}
You can use enum
, and then check if string in Enum
export enum Decisions {
approve = 'approve',
reject = 'reject'
}
export type DecisionsTypeUnion =
Decisions.approve |
Decisions.reject;
if (decision in Decisions) {
// valid
}
As of Typescript 3.8.3 there isn't a clear best practice around this. There appear to be three solutions that don't depend on external libraries. In all cases you will need to store the strings in an object that is available at runtime (e.g. an array).
For these examples, assume we need a function to verify at runtime whether a string is any of the canonical sheep names, which we all know to be Capn Frisky
, Mr. Snugs
, Lambchop
. Here are three ways to do this in a way that the Typescript compiler will understand.
Take your helmet off, verify the type yourself, and use an assertion.
const sheepNames = ['Capn Frisky', 'Mr. Snugs', 'Lambchop'] as const;
type SheepName = typeof sheepNames[number]; // "Capn Frisky" | "Mr. Snugs" | "Lambchop"
// This string will be read at runtime: the TS compiler can't know if it's a SheepName.
const unsafeJson = '"Capn Frisky"';
/**
* Return a valid SheepName from a JSON-encoded string or throw.
*/
function parseSheepName(jsonString: string): SheepName {
const maybeSheepName: unknown = JSON.parse(jsonString);
// This if statement verifies that `maybeSheepName` is in `sheepNames` so
// we can feel good about using a type assertion below.
if (typeof maybeSheepName === 'string' && maybeSheepName in sheepNames) {
return (maybeSheepName as SheepName); // type assertion satisfies compiler
}
throw new Error('That is not a sheep name.');
}
const definitelySheepName = parseSheepName(unsafeJson);
PRO: Simple, easy to understand.
CON: Fragile. Typescript is just taking your word for it that you have adequately verified maybeSheepName
. If you accidentally remove the check, Typescript won't protect you from yourself.
This is a fancier, more generic version of the type assertion above, but it's still just a type assertion.
const sheepNames = ['Capn Frisky', 'Mr. Snugs', 'Lambchop'] as const;
type SheepName = typeof sheepNames[number];
const unsafeJson = '"Capn Frisky"';
/**
* Define a custom type guard to assert whether an unknown object is a SheepName.
*/
function isSheepName(maybeSheepName: unknown): maybeSheepName is SheepName {
return typeof maybeSheepName === 'string' && maybeSheepName in sheepNames;
}
/**
* Return a valid SheepName from a JSON-encoded string or throw.
*/
function parseSheepName(jsonString: string): SheepName {
const maybeSheepName: unknown = JSON.parse(jsonString);
if (isSheepName(maybeSheepName)) {
// Our custom type guard asserts that this is a SheepName so TS is happy.
return (maybeSheepName as SheepName);
}
throw new Error('That is not a sheep name.');
}
const definitelySheepName = parseSheepName(unsafeJson);
PRO: More reusable, marginally less fragile, arguably more readable.
CON: Typescript is still just taking your word for it. Seems like a lot of code for something so simple.
This doesn't require type assertions, in case you (like me) don't trust yourself.
const sheepNames = ['Capn Frisky', 'Mr. Snugs', 'Lambchop'] as const;
type SheepName = typeof sheepNames[number];
const unsafeJson = '"Capn Frisky"';
/**
* Return a valid SheepName from a JSON-encoded string or throw.
*/
function parseSheepName(jsonString: string): SheepName {
const maybeSheepName: unknown = JSON.parse(jsonString);
const sheepName = sheepNames.find((validName) => validName === maybeSheepName);
if (sheepName) {
// `sheepName` comes from the list of `sheepNames` so the compiler is happy.
return sheepName;
}
throw new Error('That is not a sheep name.');
}
const definitelySheepName = parseSheepName(unsafeJson);
PRO: Doesn't require type assertions, the compiler is still doing all the validation. That's important to me, so I prefer this solution.
CON: It looks kinda weird. It's harder to optimize for performance.
So that's it. You can reasonably choose any of these strategies, or go with a 3rd party library that others have recommended.
Sticklers will correctly point out that using an array here is inefficient. You can optimize these solutions by casting the sheepNames
array to a set for O(1) lookups. Worth it if you're dealing with thousands of potential sheep names (or whatever).