typescript 在运行时检查字符串文字联合类型的有效性?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/36836011/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Checking validity of string literal union type at runtime?
提问by Marcus Riemer
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 variable is an instance of any of those literal strings at runtime? Something along the lines of
我有一个简单的联合类型的字符串文字,由于 FFI 调用“正常”Javascript,因此需要检查它的有效性。有没有办法确保某个变量在运行时是任何这些文字字符串的实例?类似的东西
type MyStrings = "A" | "B" | "C";
MyStrings.isAssignable("A"); // true
MyStrings.isAssignable("D"); // false
采纳答案by Facebook Staff are Complicit
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.
如果您希望能够在运行时检查程序中的多个字符串联合定义,则可以使用通用StringUnion
函数来一起生成它们的静态类型和类型检查方法。
Generic Supporting Function
通用支持函数
// 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});
};
Example Definition
示例定义
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;
Example Use
示例使用
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
.
在编译时,该Race
类型的工作方式与我们通常使用"orc" | "human" | "night elf" | "undead"
. 我们还有一个.guard(...)
函数,它返回一个值是否是联合的成员并且可以用作类型保护,以及一个.check(...)
函数,如果它有效则返回传递的值,否则抛出一个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;
}
A More General Solution: runtypes
更通用的解决方案:运行类型
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.
这种方法基于runtypes 库,它提供了类似的功能,用于在 TypeScript 中定义几乎所有类型并自动获取运行时类型检查器。对于这种特定情况,它会更冗长一些,但如果您需要更灵活的东西,请考虑检查一下。
Example Definition
示例定义
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.
示例用法与上面相同。
回答by Maarten
Since Typescript 2.1, you can do it the other way around with the keyof
operator.
从 Typescript 2.1 开始,您可以使用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!
}
回答by Limarenko Denis
You can use enum
, and then check if string in Enum
您可以使用enum
, 然后检查 Enum 中的字符串
export enum Decisions {
approve = 'approve',
reject = 'reject'
}
export type DecisionsTypeUnion =
Decisions.approve |
Decisions.reject;
if (decision in Decisions) {
// valid
}
回答by jtschoonhoven
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).
从 Typescript 3.8.3 开始,对此没有明确的最佳实践。似乎有三种不依赖于外部库的解决方案。在所有情况下,您都需要将字符串存储在运行时可用的对象中(例如数组)。
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.
对于这些示例,假设我们需要一个函数来在运行时验证字符串是否是任何规范的绵羊名称,我们都知道这些名称是Capn Frisky
, Mr. Snugs
, Lambchop
。这里有三种方法可以让 Typescript 编译器理解。
1: Type Assertion (Easier)
1:类型断言(更简单)
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.
缺点:脆弱。打字稿只是相信你已经充分验证了maybeSheepName
。如果您不小心删除了支票,Typescript 将无法保护您免受伤害。
2: Custom Type Guards (More Reusable)
2:自定义类型保护(更可重用)
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.
PRO:更可重用,稍微不那么脆弱,可以说更具可读性。
CON:Typescript is still just taking your word for it. Seems like a lot of code for something so simple.
CON:打字稿仍然只是相信你的话。对于如此简单的事情,似乎有很多代码。
3: Use Array.find (Safest, Recommended)
3:使用Array.find(最安全,推荐)
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.
PRO:不需要类型断言,编译器仍在进行所有验证。这对我很重要,所以我更喜欢这个解决方案。
CON:It looks kinda weird. It's harder to optimize for performance.
CON:看起来有点奇怪。更难优化性能。
So that's it. You can reasonably choose any of these strategies, or go with a 3rd party library that others have recommended.
就是这样了。您可以合理地选择这些策略中的任何一种,或者使用其他人推荐的 3rd 方库。
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).
Sticklers 会正确地指出,在这里使用数组是低效的。您可以通过将sheepNames
数组转换为 O(1) 查找的集合来优化这些解决方案。如果您正在处理数以千计的潜在绵羊名称(或其他名称),那么值得。
回答by greene
I use "array first" solution for my string literals then use array for checks.
我对字符串文字使用“数组优先”解决方案,然后使用数组进行检查。
const MyStringsArray = ["A", "B", "C"] as const;
type MyStrings = typeof MyStringsArray[number];
let test: MyStrings;
test = "A"; // OK
test = "D"; // compile error
MyStringsArray.includes("A"); // true
MyStringsArray.includes("D"); // compile error
MyStringsArray.includes("A" as any); // true
MyStringsArray.includes("D" as any); // false
回答by Nitzan Tomer
using type
is just Type Aliasingand it will not be present in the compiled javascript code, because of that you can not really do:
usingtype
只是类型别名,它不会出现在已编译的 javascript 代码中,因此您无法真正做到:
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:
至于你的问题isAssignable
,你可以:
function isAssignable(str: MyStrings): boolean {
return str === "A" || str === "B" || str === "C";
}
回答by Karol Majewski
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.
相反,创建可执行的 JavaScript 代码来验证您的输入。确保函数正常工作是程序员的责任。
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
.
正如@jtschoonhoven 所建议的,我们可以创建一个详尽的类型保护来检查是否有任何字符串是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.
首先,创建一个调用的函数enumerate
,以确保MyStrings
使用联合的所有成员。将来扩展联合时它应该会中断,敦促您更新类型保护。
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);
}