likes
comments
collection
share

TS中的type、interface关于索引签名的区别

作者站长头像
站长
· 阅读数 42

使用TS开发快两年了,已经感受到了强制类型约束的诸多好处。无需等到代码运行时,许多小问题在类型对齐的过程中就已经暴露出来了,近期却开发中遇到个奇怪的TS问题,经过一番探究终于搞明白了,感觉探究的过程挺有意思的,就在这里记录一下。

问题描述

  • val 是个使用Typescript的interface关键字定义类型的对象
  • prop 是带有索引签名类型的组件上的属性

使用val给组件属性prop赋值时,TS会报「val中缺少类型“string”的索引签名」的错误,无意间发现把val的类型声明的关键字从interface改为type时,就不会报索引签名的错误。那么问题来了:

使用TS定义对象类型,不写索引签名却给带有索引签名类型的对象赋值时,为什么使用interface定义的类型会报错,使用type定义的类型就不会报错?

代码大致复现下报错的场景:

test = ()=>{
  interface propType {
    [key: string ]: number;
    a: number
  };
  let prop: propType; // 带索引签名类型的属性

  interface type1 { a: number }; // 不含索引签名
  type type2 = { a: number }; // 不含索引签名
  
  const data1: type1 = { a: 1 }; // interface声明类型变量
  const data2: type2 = { a: 1 }; // type声明类型变量

  prop = data1; // Error:不能将类型“type1”分配给类型“propType”。类型“type1”中缺少类型“string”的索引签名。
  prop = data2; // 编译通过,没有报索引签名错
}

索引签名

上面的问题描述中,一直围绕『索引签名』在讨论,那么什么是索引签名呢?简单来说,它定义了对象中属性名、属性值的类型,详情可参考这里👉 索引签名

猜测原因

有了对索引签名的基本了解,我们来尝试思考下问题的答案。

猜测一:是否使用type定义类型的对象会默认带索引签名?

这是比较容易能想到的答案,如果使用type声明类型的对象val默认带索引签名,那么再给带有索引签名类型的属性prop赋值时,当然不会报错。

是否果真如此呢?下面使用代码验证一下。

分别使用interfacetype关键字定义两个不带索引签名对象的类型,使用map方法遍历两个对象中的属性时,发现两个对象都报了找不到索引签名的错误。这里可以说明,使用type定义类型的对象本身并不会默认包含索引签名。

test = () => {
  interface type1 { a: number; b: number };
  type type2 = { a: number; b: number };
  
  const data1: type1 = { a: 1; b: 2 };
  const data2: type2 = { a: 1; b: 2 };
  
  // Error:在类型"obj"上找不到具有类型为"string"的参数的索引签名
  Object.keys(data1).map(item=>data1[item]);

  // Error:在类型"obj"上找不到具有类型为"string"的参数的索引签名
  Object.keys(data2).map(item=>data2[item]);
}

猜测二:是否「使用type定义类型的val」给「使用interface定义类型的prop」赋值有特殊性?

为了更加直观的验证,列了如下一组排列组合:左列是val类型,右列是prop类型,无论使用interface还是type定义类型,val的类型都不含索引签名,prop的类型都含索引签名。

文章开头提到的场景就是第一行的情况,使用interface定义的val去给使用interface定义的prop赋值时,会报val中缺少索引签名的错误。现在分别验证每一行,使用当前行的val去给prop赋值,报错情况会如何呢?

val类型prop类型
interface Val { a: number }interface Prop { [key: string ]: number; a: number }
type Val = { a: number }interface Prop { [key: string ]: number; a: number }
interface Val { a: number }type Prop = { [key: string ]: number; a: number }
type Val = { a: number }type Prop = { [key: string ]: number; a: number }

同样使用代码验证下结果:

  test = ()=>{
    interface PropInterface { [key: string ]: number; a: number  };
    interface ValInterface { a: number };

    type PropType = { [key: string ]: number; a: number  };
    type ValType = { a: number }; 

    let propInterface: PropInterface; // 模拟带索引签名的组件属性
    let propType: PropType; // 模拟带索引签名的组件属性

    const valInterface: ValIdxInterface = { a: 1 };
    const valType: ValType = { a: 1 };

    // Error:类型“ValInterface”中缺少类型“string”的索引签名。
    propInterface = valInterface;
    // Error:类型“ValInterface”中缺少类型“string”的索引签名。
    propType = valInterface;
    
    propInterface = valType;
    propType = valType;
  }

测试结果发现:所有使用interface定义的变量在赋值时都会报错,使用type定义的都不会报错。

val类型prop类型赋值结果
interface Val { a: number }interface Prop { [key: string ]: number; a: number }类型报错❌
type Val = { a: number }interface Prop { [key: string ]: number; a: number }编译通过✅
interface Val { a: number }type Prop = { [key: string ]: number; a: number }类型报错❌
type Val = { a: number }type Prop = { [key: string ]: number; a: number }编译通过✅

搜索答案

经过以上两轮验证,似乎验证了在索引签名上type比interface有一些特殊性。尝试在网上搜索答案时,找到stackoverflow上某个讨论interface和type的区别的问题下的一个回答:这个回答中举的例子跟我文章开头提到的场景是一样的。

TS中的type、interface关于索引签名的区别

果然不止我一个人遇到了这个问题啊……

这个回答中只是陈述了在索引签名上,type跟interface会有不一样的表现,但是并没有解释原因。不过在这个网页的朝下翻看其他回答的时候,学习了一个我不了解的interface特性(如下图),这里表明interface声明的类型可以反复多次添加新的属性,但是type却不可以。

TS中的type、interface关于索引签名的区别

在vscode里测试,发现重复使用type定义类型y时,果然第二遍就报错了。

  interface x { name: string }
  interface x { age: number }

  type y = { name: string }  
  type y = { age: number } // Error:标识符“y”重复。ts(2300)

虽然是新的知识点,好像对我想求解的问题没什么帮助的亚子,继续查找答案。又搜到这stack overflow上的另一个类似的问题。这个问题其实是前面提到的stackoverflow答主提的,问题内容也是一模一样,看样子答主同样被这个问题困扰了很久……

TS中的type、interface关于索引签名的区别

这个问题下面只有一个回答,仔细看了下……发现水平有限看不懂😓 ,及时求助翻译。

TS中的type、interface关于索引签名的区别

上图翻译:Record<string, string> 与 { [key: string]: string } 相同。仅当该类型的所有属性都已知并且可以针对该索引签名进行检查时,才允许将子集分配给该索引签名类型。在你的例子中,exampleType(使用type定义的类型) 中的所有内容都可以分配给 Record<string, string>。这只能检查对象字面量类型,因为对象字面量类型一旦声明就不能更改。因此,索引签名是已知的。

相反,在你使用interface去声明变量时,它们在那一刻类型并不是最终的类型。由于interfac可以进行声明合并,所以总有可能将新成员添加到同一个interface定义的类型上。

看到最后两句话时,结合之前看到的新知识点:interface定义的类型可以多次增加属性,突然想通了我的问题。上面验证了interface可以新增属性,即interface定义类型的状态都不是最终态。type不能新增属性,所以一旦使用type定义类型,类型就是最终态不可再改变。

我们回顾下上面提到的val、prop的属性:

  interface PropType {
    [key: string ]: number;
    a: number
  }

  interface ValType { a: number } // 可能会增加属性
  
  type ValType = { a: number } // 不可再变

对于有索引签名类型的变量prop来说,使用type关键字定义类型的变量val给prop赋值,val的类型{ a: number }都是符合{ [key: string ]: number; a: number }的,两边类型可以对齐。而interface定义的类型,可能后期还会新增属性,类型可能会扩展的五花八门,例如:

  • 增加属性前:interface valType { a: number }
  • 增加属性后:interface valType { a: number; b: string; c: boolean; }

可以看出,增加属性后的类型,已经跟类型{ [key: string ]: number; a: number }对不齐了,因此必须要加上索引签名,立个“规矩”,规定以后新增的属性必须满足索引签名的条件时才能够加入,否则不可加入。如上述valType要新增[key: string ]: number,规定以后新增的属性必须满足属性名是string类型,属性值是number类型才可以加入。

声明合并

在上面翻译文案的最后一句已经提到,interface可以新增属性,是因为interface可以进行声明合并(declaration merging),在typescript中除了interface,其他类型也可以进行声明合并,详情参见👉 声明合并

“声明合并”是指编译器将针对同一个名字的两个独立声明合并为单一声明。 合并后的声明同时拥有原先两个声明的特性。 任何数量的声明都可被合并;不局限于两个声明。

总结

因interface可声明合并,声明的变量类型可新增属性,不是终态,所以在给有索引签名的类型赋值时,需增加索引签名限定新增属性的类型。而使用type声明的变量类型不可新增属性,已是最终状态,只要其属性符合被赋值变量的类型,就可以直接赋值,不会报错。