简体   繁体   English

Typescript:字符串文字类型的类型保护

[英]Typescript: Type Guard with string literal type

If I have the following code:如果我有以下代码:

interface A {
    foo: string
}

interface B extends A {
    foo: "bar"
}

interface C extends A {
    foo: "baz"
}

interface D ...

namely, one interface and a number of other interfaces that extend the original, but fix a specific property of the original to a single string literal, is it possible for me to design a type guard for interfaces that extend the original by comparing against this property in a generic way?即,一个接口和许多扩展原始接口的其他接口,但将原始接口的特定属性固定为单个字符串文字,我是否可以通过与此属性进行比较来为扩展原始接口的接口设计类型保护以一种通用的方式? Something like the following:类似于以下内容:

function isExtensionOfA<T extends A>(obj: A): obj is T {
    return (obj.foo === T.foo.literal_value)
}

TypeScript's static type system is erased when TypeScript is compiled to JavaScript, so the generic type T will not exist at runtime.当 TypeScript 被编译为 JavaScript 时,TypeScript 的 static 类型系统被擦除,因此泛型类型T在运行时将不存在。 That means there's no " T.foo " to speak of for you to check at runtime.这意味着没有“ T.foo ”可供您在运行时检查。 If you want to check a value at runtime you'll need to actually provide such a value.如果你想在运行时检查一个值,你需要实际提供这样一个值。

Here's one possible approach, where you explicitly pass in the foo value you're checking against:这是一种可能的方法,您可以在其中明确传递要检查的foo值:

function isExtensionOfA<T extends A>(
  obj: A,
  foo: T["foo"]
): obj is T {
  return (obj.foo === foo);
}

And you can see that it "works":你可以看到它“有效”:

function test(obj: A) {
  if (isExtensionOfA<B>(obj, "bar")) {
    obj.bProp.toFixed() // okay
  } else if (isExtensionOfA<C>(obj, "baz")) {
    obj.cProp.toUpperCase() // okay
  } else if (isExtensionOfA<D>(obj, "qux")) {
    obj.dProp // okay
  }
}

where the scare quotes are due to the following big problem with this approach:吓人的报价是由于这种方法存在以下大问题:

test({ foo: "bar" }); // okay at compile time, but:
// 💥 RUNTIME ERROR! obj.bProp is undefined

You can see that {foo: "bar"} was accepted by test() even though it's not a B .您可以看到{foo: "bar"}test()接受,即使它不是B That's because types in TypeScript are structural (based on the shape of the data) and not nominal (based on the name or declaration ).那是因为 TypeScript 中的类型是结构性的(基于数据的形状)而不是名义上的(基于名称声明)。 When you write T extends A it means " T has a shape compatible with A " so all you know is that it has a foo property of type string .当您编写T extends A时,它意味着“ T具有与A兼容的形状”,因此您所知道的就是它具有string类型的foo属性。 It does not mean " T is one of the interfaces I explicitly declared as extending A ".这并不意味着“ T是我明确声明为扩展A的接口之一”。 So T can be all sorts of types other than B , C or D , including anonymous types like the type of the object literal {foo: "bar"} .因此T可以是BCD以外的各种类型,包括匿名类型,如 object 文字{foo: "bar"}的类型。

Oops.哎呀。


If you really want to limit something to some enumerated set of interfaces, you're going to have to define this type explicitly as a union :如果你真的想将某些东西限制在一些枚举的接口集中,你将不得不将这种类型明确定义为一个union

type ExplicitA = B | C | D // | ...

And once you have that you could write isExtensionOfA() so that it restricts its input to ExplicitA objects:一旦你有了它,你就可以编写isExtensionOfA()以便它将其输入限制为ExplicitA对象:

function isExtensionOfA<K extends ExplicitA["foo"]>(
  obj: ExplicitA,
  foo: K
): obj is Extract<ExplicitA, { foo: K }> {
  return (obj.foo === foo);
}

and now I'm using the Extract utility type to filter ExplicitA to just the union member corresponding to the foo value passed in. Now we can make our test() function use it without even needing to manually specify the type arguments:现在我使用Extract实用程序类型ExplicitA过滤为与传入的foo值相对应的联合成员。现在我们可以让我们的test() function 使用它,甚至不需要手动指定类型 arguments:

function test(obj: ExplicitA) {
  if (isExtensionOfA(obj, "bar")) {
    obj.bProp.toFixed() // okay
  } else if (isExtensionOfA(obj, "baz")) {
    obj.cProp.toUpperCase() // okay
  } else if (isExtensionOfA(obj, "qux")) {
    obj.dProp // okay
  }
}

And now the compiler will complain about calling test() with some anonymous type that happens to be compatible with A :现在编译器会抱怨用一些恰好与A兼容的匿名类型调用test()

test({ foo: "bar" }); // error at compile time

So, that's safe, and it all "works".所以,这很安全,而且一切都“有效”。 Uh oh, scare quotes again.哦,又是恐吓引语。


The problem is that we've now gone through a lot of extra effort to do something TypeScript does natively.问题是我们现在已经付出了很多额外的努力来做一些 TypeScript 本机做的事情。 The type ExplicitA is a discriminated union where foo is the discriminant property. ExplicitA类型是一个可判别联合,其中foo判别属性。 We don't need a separate isExtensionOfA() [user-defined type guard function] https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates ).我们不需要单独的isExtensionOfA() [用户定义的类型保护函数] https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates )。 We can just check the discriminant property directly:我们可以直接检查判别式:

function test(obj: ExplicitA) {
  if (obj.foo === "bar") {
    obj.bProp.toFixed() // okay
  } else if (obj.foo === "baz") {
    obj.cProp.toUpperCase() // okay
  } else {
    obj.dProp // okay
  }
}

So you probably want to just use a discriminated union to start with.所以你可能只想使用一个可区分的联盟开始。 That implies your A interface might not be necessary (unless there are other base properties you want to inherit), and you can rename A to be the union:这意味着您的A接口可能不是必需的(除非您想要继承其他基本属性),并且您可以将A重命名为联合:

interface B {
  foo: "bar"
  bProp: number;
}

interface C {
  foo: "baz"
  cProp: string;
}

interface D {
  foo: "qux"
  dProp: boolean
}

type A = B | C | D // | ...

function test(obj: A) {
  if (obj.foo === "bar") {
    obj.bProp.toFixed() // okay
  } else if (obj.foo === "baz") {
    obj.cProp.toUpperCase() // okay
  } else {
    obj.dProp // okay
  }
}

And now things really而现在事情真的

Playground link to code 游乐场代码链接

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM