Checking validity of string literal union type at runtime?

时间:2016-04-25 08:59:39

标签: typescript

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

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

10 个答案:

答案 0 :(得分:16)

从Typescript 2.1开始,你可以用with the keyof operator的方式做到这一点。

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

如下:

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

答案 1 :(得分:13)

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

对于这些示例,假设我们需要一个函数来在运行时验证字符串是否为任何规范的绵羊名,我们都知道它们是Capn FriskyMr. SnugsLambchop 。这是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' && 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:简单,易于理解。

CON :易碎。 Typescript只是在说您的话,您已经充分验证了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' && 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:可重用性更高,更不那么脆弱,可以说更具可读性。

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);

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

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


就这样。您可以合理地选择这些策略中的任何一种,也可以使用其他人推荐的第三方库。

Sticklers会正确指出,在此处使用数组效率低下。您可以通过将sheepNames数组强制转换为O(1)查找的集合来优化这些解决方案。如果您要处理成千上万个可能的绵羊名称(或其他名称),则值得。

答案 2 :(得分:5)

如果您希望在运行时检查程序中的几个字符串联合定义,则可以使用通用的StringUnion函数来一起生成其静态类型和类型检查方法。 / p>

通用支持功能

// 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(...)函数,该函数返回值是否是联合的成员并且可以用作type 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;
}

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

此方法基于the runtypes library,该方法提供了类似的功能,用于在TypeScript中定义几乎所有类型并自动获取运行时类型检查器。对于这种特定情况,它会有些冗长,但是如果需要更灵活的方法,可以考虑进行检查。

示例定义

import {Union, Literal, Static} from 'runtypes';

const Race = Union(
  Literal('orc'),
  Literal('human'),
  Literal('night elf'),
  Literal('undead'),
);
type Race = Static<typeof Race>;

示例用法与上面相同。

答案 3 :(得分:4)

您可以使用jQuery('.featured li a').on( "click", function() { var currentHref = this.href; jQuery(".featured li a").each(function() { if (this.href == currentHref) { jQuery(this).parent().addClass("active"); } else { jQuery(this).parent().removeClass("active"); } }); }); ,然后检查Enum中的字符串

enum

答案 4 :(得分:3)

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

答案 5 :(得分:2)

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";
}

答案 6 :(得分:1)

这是我的建议:

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 的性能更高

答案 7 :(得分:0)

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

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);
}

答案 8 :(得分:0)

基于@jtschoonhoven 最安全的解决方案,可以编写通用工厂来生成解析或验证函数:

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

repl example

这里的弱点是确保我们的类型parseUnionFactory 从我们的值数组构建的方式保持一致。

答案 9 :(得分:0)

我采用了从联合类型创建新对象类型并创建对象类型的虚拟实例的方法。然后可以使用类型保护来检查字符串类型。

这方面的一个好处是,每次向联合添加/删除较新的类型时,TS 编译器也会抱怨更新对象。

$ /Users/someuser/Library/Android/sdk/ndk/21.1.6352462/toolchains/llvm/prebuilt/darwin-x86_64/bin/x86_64-linux-android24-clang++ --target=x86_64-none-linux-android24 --sysroot=/Users/someuser/Library/Android/sdk/ndk/21.1.6352462/toolchains/llvm/prebuilt/darwin-x86_64/sysroot -fPIC -g -DANDROID -fdata-sections -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -D_FORTIFY_SOURCE=2 -Wformat -Werror=format-security    -O2 -DNDEBUG  -Wl,--exclude-libs,libgcc.a -Wl,--exclude-libs,libgcc_real.a -Wl,--exclude-libs,libatomic.a -static-libstdc++ -Wl,--build-id -Wl,--fatal-warnings -Wl,--no-undefined -Qunused-arguments  -shared -Wl,-soname,libgpr.so -o libgpr.so CMakeFiles/gpr.dir/src/core/lib/gpr/alloc.cc.o CMakeFiles/gpr.dir/src/core/lib/gpr/atm.cc.o CMakeFiles/gpr.dir/src/core/lib/gpr/cpu_iphone.cc.o CMakeFiles/gpr.dir/src/core/lib/gpr/cpu_linux.cc.o CMakeFiles/gpr.dir/src/core/lib/gpr/cpu_posix.cc.o CMakeFiles/gpr.dir/src/core/lib/gpr/cpu_windows.cc.o CMakeFiles/gpr.dir/src/core/lib/gpr/env_linux.cc.o CMakeFiles/gpr.dir/src/core/lib/gpr/env_posix.cc.o CMakeFiles/gpr.dir/src/core/lib/gpr/env_windows.cc.o CMakeFiles/gpr.dir/src/core/lib/gpr/log.cc.o CMakeFiles/gpr.dir/src/core/lib/gpr/log_android.cc.o CMakeFiles/gpr.dir/src/core/lib/gpr/log_linux.cc.o CMakeFiles/gpr.dir/src/core/lib/gpr/log_posix.cc.o CMakeFiles/gpr.dir/src/core/lib/gpr/log_windows.cc.o CMakeFiles/gpr.dir/src/core/lib/gpr/murmur_hash.cc.o CMakeFiles/gpr.dir/src/core/lib/gpr/string.cc.o CMakeFiles/gpr.dir/src/core/lib/gpr/string_posix.cc.o CMakeFiles/gpr.dir/src/core/lib/gpr/string_util_windows.cc.o CMakeFiles/gpr.dir/src/core/lib/gpr/string_windows.cc.o CMakeFiles/gpr.dir/src/core/lib/gpr/sync.cc.o CMakeFiles/gpr.dir/src/core/lib/gpr/sync_abseil.cc.o CMakeFiles/gpr.dir/src/core/lib/gpr/sync_posix.cc.o CMakeFiles/gpr.dir/src/core/lib/gpr/sync_windows.cc.o CMakeFiles/gpr.dir/src/core/lib/gpr/time.cc.o CMakeFiles/gpr.dir/src/core/lib/gpr/time_posix.cc.o CMakeFiles/gpr.dir/src/core/lib/gpr/time_precise.cc.o CMakeFiles/gpr.dir/src/core/lib/gpr/time_windows.cc.o CMakeFiles/gpr.dir/src/core/lib/gpr/tls_pthread.cc.o CMakeFiles/gpr.dir/src/core/lib/gpr/tmpfile_msys.cc.o CMakeFiles/gpr.dir/src/core/lib/gpr/tmpfile_posix.cc.o CMakeFiles/gpr.dir/src/core/lib/gpr/tmpfile_windows.cc.o CMakeFiles/gpr.dir/src/core/lib/gpr/wrap_memcpy.cc.o CMakeFiles/gpr.dir/src/core/lib/gprpp/arena.cc.o CMakeFiles/gpr.dir/src/core/lib/gprpp/examine_stack.cc.o CMakeFiles/gpr.dir/src/core/lib/gprpp/fork.cc.o CMakeFiles/gpr.dir/src/core/lib/gprpp/global_config_env.cc.o CMakeFiles/gpr.dir/src/core/lib/gprpp/host_port.cc.o CMakeFiles/gpr.dir/src/core/lib/gprpp/mpscq.cc.o CMakeFiles/gpr.dir/src/core/lib/gprpp/stat_posix.cc.o CMakeFiles/gpr.dir/src/core/lib/gprpp/stat_windows.cc.o CMakeFiles/gpr.dir/src/core/lib/gprpp/thd_posix.cc.o CMakeFiles/gpr.dir/src/core/lib/gprpp/thd_windows.cc.o CMakeFiles/gpr.dir/src/core/lib/gprpp/time_util.cc.o CMakeFiles/gpr.dir/src/core/lib/profiling/basic_timers.cc.o CMakeFiles/gpr.dir/src/core/lib/profiling/stap_timers.cc.o  -ldl -lm third_party/abseil-cpp/absl/status/libabsl_status.so third_party/abseil-cpp/absl/synchronization/libabsl_synchronization.so -landroid -llog third_party/abseil-cpp/absl/strings/libabsl_str_format_internal.so third_party/abseil-cpp/absl/strings/libabsl_cord.so third_party/abseil-cpp/absl/types/libabsl_bad_optional_access.so third_party/abseil-cpp/absl/debugging/libabsl_stacktrace.so third_party/abseil-cpp/absl/debugging/libabsl_symbolize.so third_party/abseil-cpp/absl/debugging/libabsl_debugging_internal.so third_party/abseil-cpp/absl/debugging/libabsl_demangle_internal.so third_party/abseil-cpp/absl/synchronization/libabsl_graphcycles_internal.so third_party/abseil-cpp/absl/time/libabsl_time.so third_party/abseil-cpp/absl/strings/libabsl_strings.so third_party/abseil-cpp/absl/strings/libabsl_strings_internal.so third_party/abseil-cpp/absl/base/libabsl_throw_delegate.so third_party/abseil-cpp/absl/numeric/libabsl_int128.so third_party/abseil-cpp/absl/time/libabsl_civil_time.so third_party/abseil-cpp/absl/time/libabsl_time_zone.so third_party/abseil-cpp/absl/base/libabsl_malloc_internal.so third_party/abseil-cpp/absl/base/libabsl_base.so third_party/abseil-cpp/absl/base/libabsl_spinlock_wait.so third_party/abseil-cpp/absl/base/libabsl_raw_logging_internal.so third_party/abseil-cpp/absl/base/libabsl_log_severity.so -pthread -latomic -lm
/Users/someuser/Library/Android/sdk/ndk/21.1.6352462/toolchains/llvm/prebuilt/darwin-x86_64/bin/../lib/gcc/x86_64-linux-android/4.9.x/../../../../x86_64-linux-android/bin/ld: error: symbol memcpy has undefined version GLIBC_2.2.5
/Users/someuser/Library/Android/sdk/ndk/21.1.6352462/toolchains/llvm/prebuilt/darwin-x86_64/sysroot/usr/include/bits/fortify/string.h:62: error: undefined reference to 'memcpy', version 'GLIBC_2.2.5'
clang++: error: linker command failed with exit code 1 (use -v to see invocation)

用法:

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)
}