我有一个简单的字符串字面量联合类型,并且由于与“正常”的JavaScript的 FFI 调用需要检查它的有效性。是否有一种方法可以确保某个变量在运行时是这些字面字符串中的任何一个实例?类似于:
type MyStrings = "A" | "B" | "C";
MyStrings.isAssignable("A"); // true
MyStrings.isAssignable("D"); // false
我有一个简单的字符串字面量联合类型,并且由于与“正常”的JavaScript的 FFI 调用需要检查它的有效性。是否有一种方法可以确保某个变量在运行时是这些字面字符串中的任何一个实例?类似于:
type MyStrings = "A" | "B" | "C";
MyStrings.isAssignable("A"); // true
MyStrings.isAssignable("D"); // false
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' && sheepNames.includes(maybeSheepName)) {
return (maybeSheepName as SheepName); // type assertion satisfies compiler
}
throw new Error('That is not a sheep name.');
}
const definitelySheepName = parseSheepName(unsafeJson);
PRO:简单易懂。
CON:脆弱。Typescript 只是相信你已经充分验证了 maybeSheepName
,如果你不小心移除了这个检查,Typescript 将无法保护你免受错误的影响。
这是一个更高级、更通用的类型断言版本,但它仍然只是一种类型断言。
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' && sheepNames.includes(maybeSheepName);
}
/**
* 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);
优点:更加可重用,稍微不那么脆弱,可能更易读。
缺点:Typescript仍然只是相信你的话。对于如此简单的事情来说,代码量似乎有点多。
如果您(像我一样)不信任自己,这不需要类型断言。
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);
优点:不需要类型断言,编译器仍然进行所有验证。对我来说很重要,所以我更喜欢这个解决方案。
缺点:看起来有点奇怪。更难优化性能。
就是这样。您可以合理地选择任何一种策略,或者使用其他人推荐的第三方库。
一些苛刻的人会正确地指出,在这里使用数组是低效的。您可以通过将sheepNames
数组转换为一个集合,以实现O(1)查找,并优化这些解决方案。如果您在处理数千个潜在的羊名(或其他任何内容),那么这是值得的。
keyof
操作符 来实现相反的效果。// 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!
}
x in myStrings
或 myStrings.has(x)
。 - jtschoonhovenx in myStrings
实际上不能正常工作,对数组使用in
运算符将导致意外的结果。检查'x' in []
就能明白我说的是什么了...
myStrings.includes(x)
是一种方法,或者是经典的myStrings.indexOf(x) > -1
。
另外,如果你选择数组解决方案(就像你在答案中已经指出的那样),类型应该是type MyStrings = typeof myStrings[number]
。 - panepeter如果您的程序中有多个字符串联合定义,并希望在运行时进行检查,您可以使用通用的StringUnion
函数来生成它们的静态类型和类型检查方法。
// 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});
};
我们还需要一行样板代码来提取生成的类型并将其定义与命名空间对象合并。 如果此定义被导出并导入到另一个模块中,它们将自动获得合并后的定义; 消费者不需要重新提取类型。
const Race = StringUnion(
"orc",
"human",
"night elf",
"undead",
);
type Race = typeof Race.type;
在编译时,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;
}
这种方法基于runtypes库,它提供了类似的函数来定义TypeScript中几乎任何类型,并自动获取运行时类型检查器。对于这种特定情况,它可能会稍微冗长一些,但如果您需要更灵活的东西,请考虑查看它。
import {Union, Literal, Static} from 'runtypes';
const Race = Union(
Literal('orc'),
Literal('human'),
Literal('night elf'),
Literal('undead'),
);
type Race = Static<typeof Race>;
使用示例与上面相同。
enum
,然后检查字符串是否在枚举中。export enum Decisions {
approve = 'approve',
reject = 'reject'
}
export type DecisionsTypeUnion =
Decisions.approve |
Decisions.reject;
if (decision in Decisions) {
// valid
}
decision
在有效块中的类型为Decisions
。您需要添加类型断言。 - jtschoonhoven使用“数组第一个”解决方案,同时创建字符串字面量并使用Array.includes():
const MyStringsArray = ["A", "B", "C"] as const;
MyStringsArray.includes("A" as any); // true
MyStringsArray.includes("D" as any); // false
type MyStrings = typeof MyStringsArray[number];
let test: MyStrings;
test = "A"; // OK
test = "D"; // compile error
type
的使用仅仅是类型别名,并且它不会出现在编译后的 JavaScript 代码中。因此,你不能真正地执行以下操作:
MyStrings.isAssignable("A");
您可以通过以下方式使用它:
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")
}
关于你提出的关于 isAssignable
的问题,你可以:
function isAssignable(str: MyStrings): boolean {
return str === "A" || str === "B" || str === "C";
}
MyStrings
被扩展而未更新相应的isAssignable
函数,它就会失效。我希望有一种更少手动干预的解决方案。 - Marcus Riemertype
部分是严格的 TypeScript 代码,这些信息在生成的 JS 文件中会丢失。你可以使用 enum
来代替 type
。 - Nitzan Tomer我采用了从联合类型创建新对象类型的方法,并创建了一个虚拟实例。然后可以使用类型保护来检查字符串类型。
这种方法的好处是,每次向联合类型添加/删除新类型时,TS编译器都会提醒更新对象。
type MyStrings = "A" | "B" | "C";
type MyStringsObjectType = {
[key in MyStrings ] : any
}
export const myStringsDummyObject : MyStringsObjectType = {
A : "",
B : "",
C : "",
}
export const isAssignable = (type: string):type is MyStrings => {
return (type in myStringsDummyObject)
}
使用方法:
if(isAssignable("A")){ //true
}
if(isAssignable("D")){ //false
}
你不能在类型上调用方法,因为类型在运行时不存在。
MyStrings.isAssignable("A"); // Won't work — `MyStrings` is a string literal
相反,创建可执行的JavaScript代码来验证您的输入。程序员有责任确保函数正常工作。
function isMyString(candidate: string): candidate is MyStrings {
return ["A", "B", "C"].includes(candidate);
}
更新
根据 @jtschoonhoven 的建议,我们可以创建一个详尽的类型保护程序,检查任何字符串是否属于 MyStrings
中的一种。
首先,创建一个名为 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;
function isMyString(candidate: string): candidate is MyStrings {
const valid = enumerate<MyStrings>()('A', 'B', 'C');
return valid.some(value => candidate === value);
}
isMyString
函数中添加一个检查来验证["A", "B", "C"]是否符合MyStrings
,否则两者将危险地分歧。 - jtschoonhovenMyStrings
中添加另一个字符串,比如说 type MyStrings = "A" | "B" | "C" | "D"
,类型保护将返回一个错误的负结果。 - Karol MajewskimyStrings
定义为Array<MyStrings>
类型,那么如果您更新了MyStrings
类型而没有更新相应的myStrings
值,则编译器会抛出错误。因此,这样做是安全的。 - jtschoonhovenconst parseUnionFactory = <RawType, T extends RawType>(values: readonly T[]): ((raw: RawType) => T | null) => {
return (raw: RawType): T => {
const found = values.find((test) => test === raw)
if (found) {
return found
}
throw new InvalidUnionValueError(values, raw)
}
}
使用中:
const sheepNames = ['Capn Frisky', 'Mr. Snugs', 'Lambchop'] as const
type SheepName = typeof sheepNames[number]
const parseSheepName = parseUnionFactory(sheepNames)
let imaSheep: SheepName = parseSheepName('Lampchop') // Valid
let thisllThrow: SheepName = parseSheepName('Donatello') // Will throw error
这里的弱点在于确保我们的类型与parseUnionFactory
从值数组中构建的方式保持一致。
const myFirstStrings = ["A", "B", "C"] as const;
type MyFirst = typeof myFirstStrings[number];
const mySecondStrings = ["D", "E", "F"] as const;
type MySecond = typeof mySecondStrings[number];
type MyStrings = MyFirst | MySecond;
const myFirstChecker: Set<string> = new Set(myFirstStrings);
function isFirst(name: MyStrings): name is MyFirst {
return myFirstChecker.has(name);
}
这个解决方案比其他答案建议使用的Array.find
更加高效。
in
,它期望一个索引或属性名。不过使用Array.prototype.includes
似乎可以解决这个问题。 - Jameel A.Argument of type 'string' is not assignable to parameter of type '"Capn Frisky", "Mr. Snugs", "Lambchop"'.
当我从sheepNames
数组中移除as const
时,它可以正常工作。有任何想法我可能遗漏了什么吗? - HanssheepNames.includes(maybeSheepName as SheepName);
- Leedan Johnson