打字稿交集类型和功能签名未引发预期错误

时间:2018-07-13 11:38:49

标签: typescript

我声明了以下类型:

type ExampleA = {
    a: string;
}

type ExampleB = {
    b: number;
}

type ExampleC = {
    c: boolean;
}

type Examples = ExampleA &
    ExampleB &
    ExampleC;

然后我使用如下类型:

function foo(pattern: { [key: string]: string }) {
    console.log(pattern);
}

const bar: Examples = { a: 'foo', b: 1, c: false }; 
foo(bar);

即使foo(bar)变量与bar:Examples的函数签名不匹配,Typescript编译器在调用foo方法时也不会引发任何错误。

Playground link

为什么打字稿不抛出任何错误?这是编译器中的错误吗?

2 个答案:

答案 0 :(得分:5)

之所以可行,是因为交叉点类型可以为其基本类型分配。

作为交集类型,Examples可分配给ExampleAExampleA可分配给{ [key: string]: string }。因此,Examples必须可分配给功能参数类型

这可以在以下代码中显示:

const bar: Examples = { a: 'foo', b: 1, c: false }; 
const bar2: ExampleA = bar;
const bar3: { [key: string]: string } = bar2;
foo(bar3); //This works
foo(bar2); //Since the assignment bar3 = bar2 works, this must work, too
foo(bar); //Since the assignment bar2 = bar works, this must work, too

Playground version


更新

当您要坚持“当A可分配给B而B可分配给C时,则A 必须可分配给C”的原则是必然的。类型系统除了允许这些分配者外别无选择。但是,在将值作为参数传递给foo时,实际上还有另一个问题。

您可以将值分配给仅共享分配值一部分成员的类型的变量。因此,此分配工作正常:

let item: { a: string, b: number } = { a: "Hello World!", b: 1 };
let partiallyMatchingItem: { a: string } = item;

partiallyMatchingItem的属性多于类型中实际声明的属性,这绝对没有问题。保证是最低保证。

但是分配给映射类型无效,因为item是类型number的附加成员:

let item = { a: "Hello World!", b: 1 };
let mappedTypeItem: { [key: string]: string } = item; //Error

因此,这次的保证不是最低保证,而是绝对保证。当您考虑可以轻松地(有意或无意地)绕开它时,这是非常荒谬的:

let item = { a: "Hello World!", b: 1 };
let partiallyMatchingItem: { a: string } = item;
let mappedTypeItem: { [key: string]: string } = partiallyMatchingItem;

或者简单地:

let item = { a: "Hello World!", b: 1 };
let mappedTypeItem: { [key: string]: string } = item as { a: string };

这是一个等待发生的错误,尤其是当您枚举mappedTypeItem的属性并且假定所有属性的值都是string时。

考虑到TypeScript中常见的结构化类型分配,这种绝对保证不适合类型系统通常提供的最低保证系统。

一个干净的解决方案是使“常规”类型的值不能分配给映射类型(如果需要向后兼容,则可以使用tsconfig.json文件中的开关进行切换)。至少您应该避免这种分配,因为此处提供的类型安全性很弱。

答案 1 :(得分:3)

如果您确实要出错,可以将Example声明为接口,而不是交集类型。 Since 2.2,界面可以扩展对象类型(甚至交集类型)

type ExampleA = {
    a: string;
}

type ExampleB = {
    b: number;
}

type ExampleC = {
    c: boolean;
}

interface Examples extends ExampleA, ExampleB, ExampleC {

}

function foo(pattern: { [key: string]: string }) {
    console.log(pattern);
}

const bar: Examples = { a: 'foo', b: 1, c: false }; 

foo(bar); // error

甚至通过这种方式,可以更好地说明界面和交集类型之间的区别:

type Examples = ExampleA & // the same as in question
    ExampleB &
    ExampleC;

interface IExamples extends Examples { // empty interface "collapses" the intersection 
}

const bar1: Examples = { a: 'foo', b: 1, c: false };  
foo(bar1); // no error

const bar2: IExamples = { a: 'foo', b: 1, c: false };  
foo(bar2); // error

Titian在评论中建议,在交集之外构造对象类型的另一种方法是使用映射类型,该映射类型与其通用参数几乎相同,但不完全相同:

type Id<T> = { [P in keyof T]: T[P] } 

const bar3: Id<ExampleA & ExampleB & ExampleC> = { a: 'foo', b: 1, c: false };

foo(bar3); // error