跳到主要内容

类型拓宽(Type Widening)

比如对 null 和 undefined 的类型进行拓宽,通过 let、var 定义的变量如果满足未显式声明类型注解且被赋予了 null 或 undefined 值,则推断出这些变量的类型是 any:

{
let x = null; // 类型拓宽成 any
let y = undefined; // 类型拓宽成 any
/** -----分界线------- */
const z = null; // 类型是 null
/** -----分界线------- */
let anyFun = (param = null) => param; // 形参类型是 null
let z2 = z; // 类型是 null
let x2 = x; // 类型是 null
let y2 = y; // 类型是 undefined
}

注意:在严格模式下,一些比较老的版本中(2.0)null 和 undefined 并不会被拓宽成“any”

在现代 TypeScript 中,以上示例的第 2~3 行的类型拓宽更符合实际编程习惯,我们可以赋予任何其他类型的值给具有 null 或 undefined 初始值的变量 x 和 y。

示例第 7~10 行的类型推断行为因为开启了 strictNullChecks=true(基于严格模式),此时我们可以从类型安全的角度试着思考一下:这几行代码中出现的变量、形参的类型为什么是 null 或 undefined,而不是 any?因为前者可以让我们更谨慎对待这些变量、形参,而后者不能。

类型缩小(Type Narrowing)

在 TypeScript 中,我们可以通过某些操作将变量的类型由一个较为宽泛的集合缩小到相对较小、较明确的集合,这就是 "Type Narrowing"。

比如,我们可以使用类型守卫将函数参数的类型从 any 缩小到明确的类型,具体示例如下:

{
let func = (anything: any) => {
if (typeof anything === "string") {
return anything; // 类型是 string
} else if (typeof anything === "number") {
return anything; // 类型是 number
}
return null;
};
}

在 VS Code 中 hover 到第 4 行的 anything 变量提示类型是 string,到第 6 行则提示类型是 number。

同样,我们可以使用类型守卫将联合类型缩小到明确的子类型,具体示例如下:

{
let func = (anything: string | number) => {
if (typeof anything === "string") {
return anything; // 类型是 string
} else {
return anything; // 类型是 number
}
};
}

当然,我们也可以通过字面量类型等值判断(===)或其他控制流语句(包括但不限于 if、三目运算符、switch 分支)将联合类型收敛为更具体的类型,如下代码所示:

{
type Goods = "pen" | "pencil" | "ruler";
const getPenCost = (item: "pen") => 2;
const getPencilCost = (item: "pencil") => 4;
const getRulerCost = (item: "ruler") => 6;
const getCost = (item: Goods) => {
if (item === "pen") {
return getPenCost(item); // item => 'pen'
} else if (item === "pencil") {
return getPencilCost(item); // item => 'pencil'
} else {
return getRulerCost(item); // item => 'ruler'
}
};
}

在上述 getCost 函数中,接受的参数类型是字面量类型的联合类型,函数内包含了 if 语句的 3 个流程分支,其中每个流程分支调用的函数的参数都是具体独立的字面量类型。

那为什么类型由多个字面量组成的变量 item 可以传值给仅接收单一特定字面量类型的函数 getPenCost、getPencilCost、getRulerCost 呢?这是因为在每个流程分支中,编译器知道流程分支中的 item 类型是什么。比如 item === 'pencil' 的分支,item 的类型就被收缩为“pencil”。

事实上,如果我们将上面的示例去掉中间的流程分支,编译器也可以推断出收敛后的类型,如下代码所示:

const getCost = (item: Goods) => {
if (item === "pen") {
item; // item => 'pen'
} else {
item; // => 'pencil' | 'ruler'
}
};