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

Mar*_*mer 27 typescript

我有一个简单的联合类型的字符串文字,需要检查它的有效性,因为FFI调用"普通"的Javascript.有没有办法确保某个变量在运行时是任何这些文字字符串的实例?有点像

type MyStrings = "A" | "B" | "C";
MyStrings.isAssignable("A"); // true
MyStrings.isAssignable("D"); // false
Run Code Online (Sandbox Code Playgroud)

jts*_*ven 19

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

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

1:类型断言(更简单)

取下头盔,自己验证类型,然后使用断言。

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);
Run Code Online (Sandbox Code Playgroud)

优点:简单,容易理解。

缺点:脆弱。打字稿只是相信你已经充分验证过maybeSheepName。如果您不小心删除了支票,Typescript 不会保护您免受自己的伤害。

2:自定义类型保护(更可重用)

这是上述类型断言的更高级、更通用的版本,但它仍然只是一个类型断言。

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);
Run Code Online (Sandbox Code Playgroud)

PRO:更可重用,稍微不那么脆弱,可以说更具可读性。

CON:打字稿仍然只是相信你的话。对于如此简单的事情,似乎有很多代码。

3:使用Array.find(最安全,推荐)

这不需要类型断言,以防您(像我一样)不信任自己。

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);
Run Code Online (Sandbox Code Playgroud)

PRO:不需要类型断言,编译器仍在进行所有验证。这对我很重要,所以我更喜欢这个解决方案。

CON:看起来有点奇怪。更难优化性能。


就是这样了。您可以合理地选择这些策略中的任何一种,或者使用其他人推荐的 3rd 方库。

Sticklers 会正确地指出,在这里使用数组是低效的。您可以通过将sheepNames数组转换为 O(1) 查找的集合来优化这些解决方案。如果您正在处理数以千计的潜在绵羊名称(或其他名称),那么值得。

  • 正在测试第二个示例,当我从 `sheepNames` 中删除 `as const` 时,得到`'string'类型的参数不能分配给'"Capn Frisky", "Mr. Snugs", "Lambchop"'类型的参数。` -array,但它可以工作。我有什么想法吗? (7认同)
  • 由于使用了需要索引或属性名称的“in”,第二个示例中的类型保护似乎实际上不起作用。使用“Array.prototype.includes”似乎可行。 (2认同)

Maa*_*ten 18

从Typescript 2.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!
}
Run Code Online (Sandbox Code Playgroud)

  • 如果创建具有空值的对象让您感到畏缩,您还可以使用数组或集合,然后分别检查“x in myStrings”或“myStrings.has(x)”。 (3认同)
  • @jtschoonhoven “x in myStrings”实际上不起作用,在数组上使用“in”运算符会给您带来意想不到的结果。检查 [] 中的“x”以了解我的意思……“myStrings.includes(x)”将是一种方法,或者很好的“myStrings.indexOf(x) > -1”。另外,如果您选择数组解决方案(正如您在答案中已经指出的那样),则类型需要读取“type MyStrings = typeof myStrings[number]”。 (2认同)

小智 8

如果您希望在运行时检查程序中的几个字符串联合定义,则可以使用通用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});
};
Run Code Online (Sandbox Code Playgroud)

示例定义

我们还需要一行样板来提取生成的类型并将其定义与其名称空间对象合并。如果将此定义导出并导入到另一个模块中,它们将自动获取合并的定义;消费者不需要自己重新提取类型。

const Race = StringUnion(
  "orc",
  "human",
  "night elf",
  "undead",
);
type Race = typeof Race.type;
Run Code Online (Sandbox Code Playgroud)

使用范例

在编译时,该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;
}
Run Code Online (Sandbox Code Playgroud)

一个更通用的解决方案:运行类型

此方法基于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>;
Run Code Online (Sandbox Code Playgroud)

示例用法与上面相同。


Lim*_*nis 7

您可以使用enum, 然后检查 Enum 中的字符串

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

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

if (decision in Decisions) {
  // valid
}
Run Code Online (Sandbox Code Playgroud)

  • 这里的问题是,在有效块中,决策类型不受语言服务的约束。 (3认同)
  • 的确。Typescript 编译器不理解有效块中的“decision”类型为“Decisions”。您需要添加类型断言。 (2认同)

gre*_*ene 5

您可以使用“数组优先”解决方案来创建字符串文字并像往常一样使用它。并同时使用 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
Run Code Online (Sandbox Code Playgroud)