为什么即使明确设置打字稿也能扩展属性的类型?

时间:2019-12-27 00:03:17

标签: typescript

考虑这段简单的代码,它为Person定义一个接口,然后创建该类型的对象:

interface Person {
    name: string | number
}

const john: Person = {
    name: "John"
}

现在,我希望john.namestring。但是,令我惊讶的是,即使在分配了string之后,该属性remains widened as a union的类型也似乎如此:

function greet(name: string) {
    console.log(`Hello ${name}`)
}

greet(john.name) // Argument of type 'string | number' is not assignable to parameter of type 'string'.
                 //   Type 'number' is not assignable to type 'string'.(2345)

我什至试图通过强制name的类型为string来强制编译器看到光线,但无济于事:

const john: Person = {
    name: "John" as string
}

greet(john.name) // Argument of type 'string | number' is not assignable to parameter of type 'string'.
                 //   Type 'number' is not assignable to type 'string'.(2345)

有人可以阐明这种行为吗?

3 个答案:

答案 0 :(得分:1)

TypeScript扩展了类型,以使此代码出错:

interface Person {
    name: string | number
}

const john: Person = {
    name: "John"
}

john.name = 0

function greet(name: string) {
    console.log(`Hello ${name}`)
}

greet(john.name)

john的类型仅被检查为Person

答案 1 :(得分:1)

大概您想知道您的示例为何失败:

const john: Person = {
    name: "John"
}
greet(john.name) // error!
// -> ~~~~~~~~~  "(string | number) is not assignable to string"

在以下情况下有效:

const name: string | number = "John";
greet(name); // okay

或者这个:

john.name = "Still John";
greet(john.name); // okay

Control flow type analysis是TypeScript的功能,通过类型检查器可以推断出某些较宽类型的变量可以暂时视为较窄类型。因此,如果我有一个变量name声明为类型string | number,则可以为其分配任何stringnumber

name = Math.random() < 0.5 ? 0.007 : "James Millibond";

但是以下情况在没有类型断言的情况下起作用:

console.log(typeof name === "string" ? name.toUpperCase() : name.toExponential(0)); 
// 7e-3 or JAMES MILLIBOND 

在代码的name.toUpperCase()部分中,编译器将name的类型缩小为string,在代码的name.toExponential(0)部分中,编译器的范围缩小了namenumber的类型。这是实际的控制流分析。

然后,当您将stringnumber类型的值赋给string | number类型的变量时,编译器会将该变量的类型缩小为stringnumber,直到为变量分配新值为止。这也是由于控制流分析所致。


对TypeScript的控制流分析有一个重要限制:仅缩小 union 类型。 如果变量不是联合类型,则不会缩小范围。string | number是联合类型,而Person不是。是的,name Person属性为联合类型,但这并不能使Person为联合类型。

因此,分配john本身不会进行任何缩小,但是分配john.name会进行缩小。


为什么编译器不缩小非工会类型?我能找到的最接近规范答案的是控制流分析算法的实现者在a comment中:

  

缩小分配的非工会类型的问题是我们仍在思考的问题。它变得有些复杂,因为...当涉及到可选属性时,分配的类型实际上可能比声明的类型具有更少的成员。

open issue (microsoft/TypeScript#16976)建议将控制流分析范围缩小也应用于非工会类型。如果您对此有强烈的信心,则可以考虑给它一个问题,并/或描述您的用例,如果您认为这很令人信服。


好的,希望能有所帮助。祝你好运!

Playground link to code

答案 2 :(得分:-1)

参数问题

我认为您的错误来自此函数:

function greet(name: string) {
    console.log(`Hello ${name}`)
}

对于参数,您仅将其定义为string,但类型为string | number

执行以下操作,它将纠正错误:

function greet(name: string | number) { // add number here =)
    console.log(`Hello ${name}`)
}

运行方式

当您运行其中任何一个时,它将起作用:

const john: Person = {
    name: "John"
};

const sam: Person = {
    name: 1
};

输出:

greet(john.name); // Hello John
greet(sam.name); // Hello 1