keyof运算符从对象字面类型中检索不可枚举的继承属性吗?

huangapple go评论79阅读模式
英文:

Why keyof operator retreives non enumerable inherited? properties from object literal type?

问题

请考虑以下的TypeScript代码:

'use strict';

type Value = 1 | 2 ;
type Owner = 'ownerA' | 'ownerB';
type ItemType = 'itemTypeA' | 'itemTypeB';
type Item = {
  type: ItemType;
  owner: Owner;
  value: Value;
};

type AnotherType = {
  [k in keyof Item]: null | {
    [m in keyof Item[k]]: number;
  };
};

const v: AnotherType = {
  type: { itemTypeA: 0, itemTypeB: 1,},
  owner: { ownerA: 0, ownerB: 1,},
  value: null,
};

console.log(v);

在这种情况下,tsc 抛出了一个错误:

error TS2322: Type '{ itemTypeA: number; itemTypeB: number; }' is not
assignable to type '{ [x: number]: number; toString: number; charAt:
number; charCodeAt: number; concat: number; indexOf: number;
lastIndexOf: number; localeCompare: …

显然,[m in keyof Item[k]]: number; 迭代出现了某种原因,包括了非枚举的,甚至不存在的属性(不确定对象类型是否可以继承自 Object.prototype 的类型),比如 charCodeAt、charAt 等等。

根据我所了解的情况,这与 关于 'keyof' 类型操作符的文档 矛盾,文档描述如下:

keyof 操作符接受一个对象类型并生成其键的字符串或数字字面量联合。以下类型 P 与类型 P = "x" | "y" 相同:`type Point = { x: number; y: number }; 
type P = keyof Point;`

即,生成的联合中没有 'charAt' 等值,只有 'x' 和 'y'。

也许与 `in` 操作符有关,但再次强调,在 JavaScript 中,它只遍历对象的可枚举属性(不确定在 TypeScript 中是否作为类型操作符使用时也是如此)。

有人能否解释一下发生了什么?

注: 以上是您提供的代码的翻译部分,不包括问题的回答。

英文:

Please, consider this TypeScript code:

'use strict';

type Value = 1 | 2 ;
type Owner = 'ownerA' | 'ownerB';
type ItemType = 'itemTypeA' | 'itemTypeB';
type Item = {
  type: ItemType;
  owner: Owner;
  value: Value;
};

type AnotherType = {
  [k in keyof Item]: null | {
    [m in keyof Item[k]]: number;
  };
};

const v: AnotherType = {
  type: { itemTypeA: 0, itemTypeB: 1,},
  owner: { ownerA: 0, ownerB: 1,},
  value: null,
};

console.log(v);

The tsc throws an error in this case:

> error TS2322: Type '{ itemTypeA: number; itemTypeB: number; }' is not
> assignable to type '{ [x: number]: number; toString: number; charAt:
> number; charCodeAt: number; concat: number; indexOf: number;
> lastIndexOf: number; localeCompare: …

Apparently the [m in keyof Item[k]]: number; iteration for some reason includes the non-enumerable, even non-present properties (not sure if an object type can inherit from Object.prototype's type) like: charCodeAt, charAt etc.

Up to my knowledge this contradicts the documentation on the 'keyof' type operator, which is described as:

> The keyof operator takes an object type and produces a string or
> numeric literal union of its keys. The following type P is the same
> type as type P = "x" | "y": type Point = { x: number; y: number };
> type P = keyof Point;

i.e. there are no 'charAt' et al. values in the produced union, only 'x' and 'y'.

May be it has something to do with the in operator, but again, in JS it traverses only thru enumerable properties of an object (not sure if that's true in TypeScript when it's used as a type operator).

Could someone please explain, what's happening here?

答案1

得分: 2

你应该将你的类型更改为类似这样的:

type AnotherType = {
    [K in keyof Item]: null | {
        [M in Item[K]]: number; // <-- no keyof
    };
};

在这里,你不应该在嵌套的映射类型内使用 keyof 操作符。

M in keyof Item[K] 的问题在于 Item[K] 是一个索引访问类型,因此它是 Item 接口的 ItemTypeOwnerValue 中的一个,这些已经是你想要的键样的字面类型的联合。而且,由于 Item[K] 已经是键样的,写 keyof Item[K] 实际上是在评估键的键。假设你不关心字符串键如 "ownerA"(例如,"toUpperCase""length" 等)或数字键如 1(例如,"toFixed")。相反,你应该直接迭代 Item[K]。毕竟,我们想要的是 {[M in Owner]: number},而不是 {[M in keyof Owner]: number}

在我们进行这个更改后,AnotherType 等同于:

type AnotherType = {
    type: {
        itemTypeA: number;
        itemTypeB: number;
    } | null;
    owner: {
        ownerA: number;
        ownerB: number;
    } | null;
    value: {
        1: number;
        2: number;
    } | null;
}

如你所期望的那样。

英文:

You should change your type to something like

type AnotherType = {
    [K in keyof Item]: null | {
        [M in Item[K]]: number; // <-- no keyof
    };
};

where you don't use the keyof operator inside the nested mapped type.

The problem with M in keyof Item[K] is that Item[K] is an indexed access type into the Item interface, and thus one of ItemType, Owner, or Value, which are already the unions of key-like literal types you want. And since Item[K] is already keylike, writing keyof Item[K] is evaluating keys-of-keys. Presumably you don't care about the keys of strings like "ownerA" (e.g., "toUpperCase", "length", etc) or the keys of numbers like 1 (e.g., "toFixed"). Instead you should just iterate over Item[K] directly. After all, we want {[M in Owner]: number}, nut {[M in keyof Owner]: number}.

After we make that change, AnotherType is equivalent to

type AnotherType = {
    type: {
        itemTypeA: number;
        itemTypeB: number;
    } | null;
    owner: {
        ownerA: number;
        ownerB: number;
    } | null;
    value: {
        1: number;
        2: number;
    } | null;
}

as desired.

Playground link to code

huangapple
  • 本文由 发表于 2023年8月4日 00:36:23
  • 转载请务必保留本文链接:https://go.coder-hub.com/76830048.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定