Typescript 在运行时检查字符串文字联合类型的有效性?

Typescript 在运行时检查字符串文字联合类型的有效性?,typescript,Typescript,我有一个简单的字符串文本联合类型,需要检查它的有效性,因为FFI调用了“普通”Javascript。有没有办法确保某个变量在运行时是这些文本字符串的实例?类似于 type MyStrings = "A" | "B" | "C"; MyStrings.isAssignable("A"); // true MyStrings.isAssignable("D"); // false 使用type只是一种简单的方法,它不会出现在已编译的javascript代码中,因为您无法真正做到: MyString

我有一个简单的字符串文本联合类型,需要检查它的有效性,因为FFI调用了“普通”Javascript。有没有办法确保某个变量在运行时是这些文本字符串的实例?类似于

type MyStrings = "A" | "B" | "C";
MyStrings.isAssignable("A"); // true
MyStrings.isAssignable("D"); // false

使用
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")
}
关于
可分配的问题,您可以:

function isAssignable(str: MyStrings): boolean {
    return str === "A" || str === "B" || str === "C";
}

由于TypeScript2.1,您可以用另一种方法来实现

想法如下。由于字符串文字类型信息在运行时不可用,您将定义一个带有键的普通对象作为字符串文字,然后创建该对象的键类型

详情如下:

// 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!
}

您可以使用
enum
,然后检查enum中是否有字符串

export enum Decisions {
    approve = 'approve',
    reject = 'reject'
}

export type DecisionsTypeUnion =
    Decisions.approve |
    Decisions.reject;

if (decision in Decisions) {
  // valid
}

如果您的程序中有多个字符串联合定义,希望能够在运行时进行检查,则可以使用泛型
StringUnion
函数一起生成它们的静态类型和类型检查方法

一般支持函数 示例使用 在编译时,
Race
类型的工作原理与我们通常使用
“orc”|“human”|“暗夜精灵”|“亡灵”
定义字符串并集的工作原理相同。我们还有一个
.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;
}
更通用的解决方案:运行类型 这种方法基于,它提供了类似的函数来定义TypeScript中的几乎任何类型,并自动获取运行时类型检查器。对于这个特定的情况,它会有点冗长,但是如果需要更灵活的东西,请考虑检查它。

示例定义
从“runtypes”导入{Union,Literal,Static};
常量种族=联合(
文字('orc'),
字面意思(“人”),
字面意思(“暗夜精灵”),
字面意思(“不死生物”),
);
类型种族=静态;

示例用法与上面相同。

不能对类型调用方法,因为类型在运行时不存在

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所建议的,我们可以创建en穷举类型的保护,它将检查任何字符串是否是
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;
type ValueOf=T[keyof T];
类型包括每个=
T扩展了
? 真的
:假;
每小时输入一次=
includeEvery扩展为true
? U
:从不;
导出常量枚举=()=>
(…元素:WhenIncludesEvery):U=>元素;
新型和改进型护罩:

function isMyString(candidate: string): candidate is MyStrings {
  const valid = enumerate<MyStrings>()('A', 'B', 'C');

  return valid.some(value => candidate === value);
}
函数isMyString(候选者:string):候选者是MyString{
const valid=enumerate()('A','B','C');
返回valid.some(value=>candidate==value);
}

从Typescript 3.8.3开始,没有明确的最佳实践。似乎有三种解决方案不依赖于外部库。在所有情况下,您都需要将字符串存储在运行时可用的对象中(例如数组)

对于这些示例,假设我们需要一个函数在运行时验证一个字符串是否是任何一个规范的羊名,我们都知道它是
Capn Frisky
Mr.Snugs
Lambchop
。这里有三种方法,Typescript编译器可以理解

1:类型断言(更容易) 脱下头盔,自己验证类型,并使用断言

const sheepNames=['Capn Frisky','Mr.Snugs','Lambchop']作为const;
类型SheepName=类型sheepNames[编号];//“Capn Frisky”|“Snugs先生”|“Lambchop”
//此字符串将在运行时读取:TS编译器无法知道它是否为SheepName。
const unsafeJson=''Capn Frisky';
/**
*从JSON编码的字符串或抛出返回有效的SheepName。
*/
函数parseSheepName(jsonString:string):SheepName{
const-maybeSheepName:unknown=JSON.parse(jsonString);
//此if语句验证'maybeSheepName'是否在'sheepNames'so中
//使用下面的类型断言,我们会感觉很好。
if(类型可能是表单名称=='string'&&sheepNames.includes(可能是表单名称)){
return(可能是SheepName作为SheepName);//类型断言满足编译器的要求
}
抛出新错误('这不是羊名');
}
const definitelySheepName=parseSheepName(unsafeJson);
PRO:简单易懂

缺点:易碎。Typescript只是相信您已经充分验证了
maybeSheepName
。如果您不小心删除了该复选框,Typescript将无法保护您

2:定制型防护装置(可重复使用) 这是上面类型断言的一个更奇特、更通用的版本,但它仍然只是一个类型断言

const sheepNames=['Capn Frisky','Mr.Snugs','Lambchop']作为const;
类型羊名=羊名类型[编号];
const unsafeJson=''Capn Frisky';
/**
*定义自定义类型保护以断言未知对象是否为SheepName。
*/
函数isSheepName(maybeSheepName:未知):maybeSheepName是SheepName{
返回类型maybeSheepName=='string'&&sheepNames.includes(maybeSheepName);
}
/**
*从JSON编码的字符串或抛出返回有效的SheepName。
*/
函数parseSheepName(jsonString:string):SheepName{
const-maybeSheepName:unknown=JSON.parse(jsonString);
如果(isSheepName)(
function isMyString(candidate: string): candidate is MyStrings {
  return ["A", "B", "C"].includes(candidate);
}
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);
}
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
const 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
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
   
}
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);
}