typeScript 为什么访问一个对象的某个键会被推断为 never ?

这是一个有关 TypeScript 类型推断的问题,代码如下

interface Form {
  name: string,
  age: number

  height?: number
  address?: number
}

const blankForm = {
  name: '',
  age: 24
}

const getBlankForm = (params?: Form): Form => {
  const res: Form = { ...blankForm }
  if (!params) return res

  Object.keys(res).map(key => {
    if (params[key as keyof Form] !== undefined)
      res[key as keyof Form] = params[key as keyof Form]
  })

  return res
}

代码的倒数第 3res[key as keyof Form] = params[key as keyof Form] 报错了,提示“Type 'string | number | undefined' is not assignable to type 'never'. Type 'undefined' is not assignable to type 'never'.ts(2322)”。具体地请看下图:

image.png

这里为什么对 res 键的访问为什么会被推断成 never 呢?

更新
有朋友就业务逻辑提出了解决方案,其实在业务上这个问题是完全可以被解决的,最简单的方法就是不使用 tsc 的检查,但问题到了这一步我单纯是对这个报错感到好奇和不解。谢谢各位的批评和意见。

阅读 3k
2 个回答

谢邀,但是我确实不太清楚为什么是 never。

如果把 keyof Form 计算出来,

type FormKey = Exclude<keyof Form, never>;
// 可以从 Hint 中看到:
// type FormKey = "name" | "age" | "height" | "address"

这种情况下,如果有一个确定值的 key 是不会有问题的

  let k1: keyof Form = "name";
  res[k1] = "";

但是如果 key 只知道类型是 FormKey,值不并确定的情况,比如函数中

  function set(key: FormKey) {
    res[key] = '';
  }

就会得到不能赋值给 never 的错误提示。

为什么是 never 不太清楚,但是可以理解。你想,这时候 key 只知道是 FormKey 联合类型中一种,但具体是哪一种并不清楚。所以 res[key] 也就是 Form 属性值的类型是哪一个也不清楚。由于不同类型不能安全赋值,所以这里只能判定为不可赋值了。也许不能赋值的描述用 never 相对更贴合一些,所以用了这样的提示。

做个实验,把 Form 所有属性都改为 string

interface Form {
  name: string,
  age: string

  height?: string
  address?: string
}

你会发现 res[key] = params[key] 仍然报错,但 res[key] = params[key] ?? "" 是不会报错的。因为右边的值已经确定是 string 类型,而 Form 中无论哪一个属性,都可以把 string 类型的值赋给它。

最后说一下这种问题怎么解决 ——

由于类型的不确定性,TypeScript 也不能通过静态类型的计算来获得准确的结果,那就只有报错。实际上,在运行时类型是可以确定的,所以这个问题在代码逻辑保证不会出现异常的情况下,可以转为 any 类型来处理,在运行时通过运行逻辑保证其正确性。其他语言的“反射”也是通过运行时类型计算/判断来保证正确性的,毕竟这事儿编译器真的干不了。

// NOTE 运行时保证类型正确
(res[key] as any) = params[key]

类型计算不是万能的,不要把所有心思都花在类型计算上去。TypeScript 的类型运算已经非常复杂了,而且缺少调试工具,请适当的考虑动态类型检查。


最后再补一个试验,看起来很扯蛋 —— 其实他真的很扯蛋。但它能说明为什么会存在 m[key] !== m[key] 的可能性

const model = {
  name: "james",
  age: 20
};

type MK = keyof typeof model;

const it = {
  key: "name" as MK
};

Reflect.defineProperty(it, "key", (() => {
  let key = "age";
  return {
    get() {
      return key = (key === "age" ? "name" : "age")
    }
  }
})());

(model[it.key] as any) = model[it.key];
// 请问:现在 model 长啥样子?
console.log(model);
interface Form {
  name: string;
  age: number;

  height?: number;
  address?: number;
}

const blankForm = {
  name: "",
  age: 24,
};
const getBlankForm = (params?: Form): Form => {
  const res: Form = { ...blankForm };
  if (!params) return res;

  Object.keys(res).map((key) => {
    if (params.hasOwnProperty(key)) {
      res[key] = params[key];
    }
  });
  return res;
};
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
logo
Microsoft
子站问答
访问
宣传栏