打字稿:映射类型中的索引签名

Posted

技术标签:

【中文标题】打字稿:映射类型中的索引签名【英文标题】:Typescript: index signatures in mapped type 【发布时间】:2021-03-06 05:17:18 【问题描述】:

我怎样才能使用 'k': number, [s: string]: any 类型并抽象'k'number?我想有一个类型别名 T 这样 T<'k', number> 给出上述类型。


考虑以下示例:

function f(x:  'k': number, [s: string]: any )                            // ok
type T_no_params =  'k': number, [s: string]: any ;                         // ok
type T_key_only<k extends string> =  [a in k]: number ;                     // ok
type T_value_only<V> =  'k': V, [s: string]: any;                           // ok
type T_key_and_index<k extends string, V> =  [a in k]: V, [s: string]: any ;// ?
直接使用 'k': number, [s: string]: any作为函数f的参数类型有效。 在type-alias 中使用[s: string]: any 索引部分有效 在type-alias 中使用k extends string 也可以 将k extends string[s: string]: any 组合在同一个type-alias 中时,我收到一个解析错误(甚至不是语义错误,它甚至不是有效的语法)。

这似乎可行:

type HasKeyValue<K extends string, V> =  [s: string]: any  &  [S in K]: V 

但是在这里,我不太明白为什么它不抱怨额外的属性(&amp; 右侧的类型不应该允许具有额外属性的对象)。


编辑

在答案中多次提到&amp; 是交集运算符,它的行为应该类似于集合论交集。然而,在处理额外属性时,情况并非如此,如下例所示:

function f(x: a: number);
function g(y: b: number);
function h(z: a: number & b: number);

f(a: 42, b: 58);  // does not compile. a: 42, b: 58 is not of type a: number
g(a: 42, b: 58);  // does not compile. a: 42, b: 58 is not of type b: number
h(a: 42, b: 58);  // compiles!

在这个例子中,似乎a: 42, b: 58 既不是a: number 类型,也不是b: number 类型,但它以某种方式结束于a: number &amp; b: number 的交叉点。这不是集合论交集的工作原理。

这正是我自己的&amp;-proposal 看起来如此可疑的原因。如果有人能详细说明如何将映射类型与 [s: string]: any “相交”可以使类型“更大”而不是更小,我将不胜感激。


我看过问题

Index signature for a mapped type in Typescript How do I add an index signature for a mapped type

但它们似乎没有直接关系,尽管名称相似。

【问题讨论】:

&amp; 右侧的类型本身不允许额外的属性。通过使用(诚然名字不好的)交集类型运算符&amp;,您声明了一个在所谓的交集中具有所有类型的所有属性的类型,即[S in K]: V 以及所有[s: string]: any。跨度> 类型系统 navel gazing 没有什么问题,大声笑,但是您是否有一个要解决的实际问题导致您提出这个问题?也许发布一些除了签名之外的假设代码将有助于阐明您希望如何利用类型系统来实现更具体的东西。您是否希望在调用方、在使用签名的函数内、方便的类型声明等方面提供更好的智能感知?解决问题的惯用方法可能不需要这种特殊方法。 @rob3c 不确定你所说的“肚脐凝视”是什么意思——我从来没有为打字稿编译器做过贡献。最初的问题非常实用:我不得不将看起来很奇怪的[s: string]: any 添加到多种类型中,以便在我不想要的地方停用额外属性检查,我在问自己是否可以提取@987654353 @part 转换成单独的类型定义,以减少混乱。 不确定为什么要提到对 TS 编译器的贡献?原始问题只有声明/签名,没有具体的使用示例,这可能仅意味着理论而非实际兴趣。我并不是说没有实际应用。正如我所说,理论问题是可以的。我建议描述假设的用法,以防您还试图解决实际问题。一个原因是人们经常在他们的问题中采用解决方案的形式,而不是更直接地陈述问题。赏金还询问了惯用用法。 对于那些未在编辑中编译的函数示例,您将对象文字作为参数传递。对象字面量很特殊,只能指定已知属性。我相信这个想法是丢弃额外的属性数据可能是一个错误,因为它不会在那个时候出现在其他任何地方。但是,您可以将带有额外 props 的变量传递给这些函数,只要它们的 props 子集与签名匹配。即const ab = a: 42, b: 58 可以作为f(ab); g(ab); h(ab); 传递而不会出现编译错误。 【参考方案1】:

type HasKeyValue&lt;K extends string, V&gt; = [s: string]: any &amp; [S in K]: V 是定义您所追求的类型的正确方法。但要知道的一件事是 (paraphrasing deprecated flag: keyofStringsOnly):

keyof 类型运算符返回字符串 |当应用于具有字符串索引签名的类型时,数字而不是字符串。

我不知道将索引限制为 string 类型而不是 string | number 的方法。实际上允许number 访问string 索引似乎是一件合理的事情,因为它符合javascript 的工作原理(人们总是可以对数字进行字符串化)。另一方面,您不能安全地访问带有字符串值的数字索引。


&amp; 类型运算符的工作方式与设置理论交集类似 - 它始终限制可能值的集合(或保持它们不变,但从不扩展)。在您的情况下,该类型不包括任何非字符串类键作为索引。准确地说,您将 unique symbol 排除在索引之外。

我认为您的困惑可能来自 Typescript 处理函数参数的方式。使用显式定义的参数调用函数的行为与将参数作为变量传递不同。在这两种情况下,Typescript 都会确保所有参数都具有正确的结构/形状,但在后一种情况下,它还不允许额外的道具。


Code illustrating the concepts:

type HasKeyValue<K extends string, V> =  [s: string]: any  &  [S in K]: V ;
type WithNumber = HasKeyValue<"n", number>;
const x: WithNumber = 
  n: 1
;

type T = keyof typeof x; // string | number
x[0] = 2; // ok - number is a string-like index
const s = Symbol("s");
x[s] = "2"; // error: cannot access via symbol

interface N 
  n: number;


function fn(p: N) 
  return p.n;


const p1 = 
  n: 1
;

const p2 = 
  n: 2,
  s: "2"
;

fn(p1); // ok - exact match
fn(p2); // ok - structural matching:  n: number  present;  additional props ignored
fn( n: 0, s: "s" ); // error: additional props not ignore when called explictily
fn(); // error: n is missing

编辑

对象文字 - 显式创建某种形状的对象,如 const p: a: number = a: 42 ,Typescript 以特殊方式处理。与常规结构推断相反,类型必须完全匹配。老实说,这是有道理的,因为那些额外的属性 - 没有额外的可能不安全的演员 - 无论如何都无法访问。

[...] 但是,TypeScript 的立场是,这段代码可能存在错误。对象文字在将它们分配给其他变量或将它们作为参数传递时会得到特殊处理并进行过多的属性检查。如果对象字面量具有“目标类型”所没有的任何属性,则会出现错误。 [...] 绕过这些检查的最后一种方法(可能有点令人惊讶)是将对象分配给另一个变量。

TS Handbook

解决此错误的另一个选择是...与 [prop: string]: any 相交。

More code:

function f(x:  a: number ) 
function g(y:  b: number ) 
function h(z:  a: number  &  b: number ) 

f( a: 42, b: 58  as  a: number ); // compiles - cast possible, but `b` inaccessible anyway
g( a: 42  as  b: number ); // does not compile - incorrect cast; Conversion of type ' a: number; ' to type ' b: number; ' may be a mistake
h( a: 42, b: 58 ); // compiles!

const p = 
  a: 42,
  b: 58
;

f(p); // compiles - regular structural typing
g(p); // compiles - regular structural typing
h(p); // compiles - regular structural typing

const i:  a: number  =  a: 42, b: 58 ; // error: not exact match
f(i); // compiles
g(i); // error
h(i); // error

【讨论】:

感谢您的回答。我认为第 3 和第 4 段(第二个水平线分隔块)描述了我发现最令人困惑的内容。我在原始问题中添加了一个编辑以强调这一点。如果您能在这个方向上进一步详细说明,我将不胜感激:为什么通过与[s: string]: any 相交来限制 [s in K]: V 类型,我们获得了一个接受更多(而不是更少)可能参数的参数类型?【参考方案2】:

这是一种关于交集运算符的推理方式。也许有帮助:

type Intersection =  a: string  &  b: number 

您可以将Intersection 解读为“具有a 类型string 的属性的对象 属性b 类型number ”。 这恰好也描述了这种简单的类型:

type Simple =  a: string; b: number 

并且这两种类型是兼容的。您几乎可以将一个替换为另一个。

我希望这可以解释为什么 HasKeyValue 确实与您尝试定义的类型相同。

至于T_key_and_index为什么不起作用,是因为第一部分[a in k]: V定义了一个mapped type,并且在映射类型的定义中不能有额外的属性。如果您需要为映射类型添加额外的属性,您可以创建一个type intersection with &amp;

【讨论】:

我这里的交集问题通过以下示例演示:function f(x: a: number); function g(y: b: number); function h(z: a: number &amp; b: number); f(a: 42, b: 58); g(a: 42, b: 58); h(a: 42, b: 58); 此示例中的前两个函数调用无法编译。因此,a: 42, b: 58 既不是a: number 类型,也不是b: number,但不知何故,它最终出现在a: numberb: number 的“交叉点”中。与映射的 [x in y]: z -types 类似:&amp; 以某种方式停用“额外属性”-检查,但为什么呢?

以上是关于打字稿:映射类型中的索引签名的主要内容,如果未能解决你的问题,请参考以下文章

编译打字稿时如何防止错误“对象类型的索引签名隐式具有'任何'类型”?

打字稿中迭代的索引签名

如何使用 JSDoc 记录符号索引签名以符合打字稿?

实现类中的打字稿索引签名和方法不起作用

查找累积和反应打字稿

typescript 打字稿索引签名示例