为什么 Typescript 无法弄清楚我的代码中的类型? [英] Why can Typescript not figure out the type in my code?

查看:34
本文介绍了为什么 Typescript 无法弄清楚我的代码中的类型?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

为什么 Typescript 编译器会抱怨以下代码?

Why does the Typescript compiler complain about the following code?

type Foo = {
  a: string
}

type Bar = {
  b: number
}

type Baz = Foo | Bar;

function f(x: Baz): number {
  if (x.a) { // property 'a' does not exist on type Bar!
    return 0;
  }

  if (x.b) { // property 'b' does not exist on type Foo!
    return 1;
  }

  return -1;
}

链接到游乐场

推荐答案

为什么编译器不能(或不会)允许这些属性访问

考虑在 jcalz 的评论中链接的 github 线程中提到的以下案例:

interface Vec2 {
  x: number
  y: number
}

interface Vec3 {
  x: number
  y: number
  z: number
}

const m = { x: 0, y: 0, z: "hello world" };
const n: Vec2 = m; // N.B. structurally m qualifies as Vec2!
function f(x: Vec2 | Vec3) {
  if (x.z) return x.z.toFixed(2); // This fails if z is not a number!
}
f(n); // compiler must allow this call

游乐场

这里代码的作者做了一个不幸的假设,仅仅因为一个属性存在且真实,它就是某种类型.但这是双重错误的:您可能有正确类型的假值(在这种情况下为零或 NaN)或不同类型的真值.但还有一些更微妙的问题:

Here the author of the code makes an unfortunate assumption that just because a property is present and truthy that it is a certain type. But this is doubly wrong: you could have a falsey value of the correct type (zero or NaN in this case) or a truthy value of a different type. But there are subtler gotchas:

type Message =
  { kind: "close" } |
  { kind: "data", payload: object }

function handle(m: Message) {
  switch (m.kind) {
    case "close":
      console.log("closing!");
      // forgot 'break;' here
    case "data":
      updateBankAccount(m.payload);
  }
}

在这种情况下,您希望编译器抱怨意外的属性访问,而不仅仅是默默地传播undefined.抓住这类事情是我们首先使用静态分析的重要原因.

This is a scenario where you'd want the compiler to complain about an unintended property access, not just silently propagate undefined. Catching this sort of thing is a big part of why we use static analysis in the first place.

Typescript 编译器已经是一项了不起的工程壮举,它不仅是一种动态语言,而且是一种-动态语言.您在此处查找的内容称为类型缩小,您可以在其中获取可能不止一种类型的值,然后将其缩小到特定类型.TS 编译器支持(至少)五种不同的习惯用法来实现这一点:

The Typescript compiler is already a marvelous feat of engineering layering a static type system on top of not just a dynamic language but an ultra-dynamic language. What you're looking for here is called type narrowing, where you take a value that could possibly be more than one type and then narrow it down to a specific type. The TS compiler supports (at least) five different idioms to achieve this:

  1. instanceof 运算符.
  2. typeof 运算符.
  3. in 运算符.
  4. 用户定义的类型保护.
  5. 一个歧视工会.
  1. The instanceof operator.
  2. The typeof operator.
  3. The in operator.
  4. A user-defined type guard.
  5. A discriminated union.

让我们依次看看:

这个很适合用户定义的类:

This one works well for user-defined classes:

class A {
  public a: number
  constructor () {
    this.a = 4;
  }
}

class B {
  public b: number
  constructor () {
    this.b = 5;
  }
}

type AB = A | B;
function abba(x: AB): number {
  if (x instanceof A) return x.a;
  if (x instanceof B) return x.b;
  return 0;
}

游乐场

这个非常适合 JS 原语(未定义、数字、字符串、布尔值等).

This one works well for JS primitives (undefined, numbers, strings, booleans, etc).

type snumber = string | number;

function f(x: snumber): string {
  if (typeof x === 'number') {
    return x.toFixed(2); // strings don't have toFixed
  } else {
    return x.repeat(2);  // numbers don't have repeat
  }
}

游乐场

这个很适合结构类型的对象:

This one works well for structurally typed objects:

type A = {
  a: number
}

type B = {
  b: string
}

type AB = A | B;

function f(x: AB): number {
  if ('a' in x) return x.a;
  if ('b' in x) return 5;
  return 0;
}

游乐场

精明的读者会注意到这与上面第一个激励示例存在相同的问题,即对象上属性的存在并不能以任何方式保证类型.这是 TS 团队的一个务实决定,它允许一个简单的选择加入习语的不常见行为 wanting 获得值或 undefined,以及很多就像演员表是一个隐含的承诺,程序员将对可能的结果负责.

An astute reader will notice that this has the same problems as the first motivating example above, namely that the existence of a property on an object does not in any way guarantee the type. This was a pragmatic decision on the part of the TS team to allow an uncommon behavior for a simple opt-in idiom of wanting to get either the value or undefined, and much like a cast is an implicit promise that the programmer is taking responsibility for the possible outcome.

这几乎适用于任何事情,但比之前的选项更冗长.这个直接来自 TS 手册:

This works well for just about anything, but is more verbose than the earlier options. This one is straight from the TS Handbook:

function isFish(pet: Fish | Bird): pet is Fish { // note the 'is'
  return (pet as Fish).swim !== undefined;
}

let pet = getSmallPet();

if (isFish(pet)) {
  pet.swim();
} else {
  pet.fly();
}

歧视工会

当您有一堆非常相似的对象,而这些对象仅在单个属性的(静态可知!)值上有所不同时,此方法效果最佳:

Discriminated union

This works best when you have a bunch of very similar objects that differ only in the (statically knowable!) value of a single property:

type A = {
  a: string
  kind: 'is-an-a'
}

type B = {
  b: number
  kind: 'is-a-b'
}

type AB = A | B;

function f(x: AB): string {
  switch (x.kind) {
    case 'is-an-a': return x.a;
    case 'is-a-b':  return '' + x.b;
  }
}

请注意,正如我所说,您需要使判别式(在本例中为 kind 属性)成为静态已知值,通常是字符串文字或枚举的成员.您不能使用变量,因为它们的值在编译时是未知的.

Note that you will as I said need to make the discriminant (the kind property in this case) a statically knowable value, usually a string literal or a member of an enum. You can't use variables, because their values aren't known at compile-time.

游乐场

所以总而言之,Typescript 编译器可以解决这个问题,你只需要使用一个它可以静态验证的习惯用法,而不是一个它不能验证的习惯用法,它为你提供了相当多的选项.

So in summary the Typescript compiler can figure it out, you just have to use an idiom it can statically verify instead of one that it can't, and it gives you a fair number of options.

这篇关于为什么 Typescript 无法弄清楚我的代码中的类型?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆