跳到主要内容

类型别名

接口类型的一个作用是将内联类型抽离出来,从而实现类型可复用。其实,我们也可以使用类型别名接收抽离出来的内联类型实现复用。

此时,我们可以通过如下所示“type 别名名字 = 类型定义”的格式来定义类型别名。

/** 类型别名 */
{
type LanguageType = {
/** 以下是接口属性 */
/** 语言名称 */
name: string;
/** 使用年限 */
age: () => number;
};
}

在上述代码中,乍看上去有点像是在定义变量,只不过这里我们把 let 、const 、var 关键字换成了 type 罢了。

此外,针对接口类型无法覆盖的场景,比如组合类型、交叉类型,我们只能使用类型别名来接收,如下代码所示:

{
/** 联合 */
type MixedType = string | number;
/** 交叉 */
type IntersectionType = { id: number; name: string } & {
age: number;
name: string;
};
/** 提取接口属性类型 */
type AgeType = ProgramLanguage["age"];
}

在上述代码中,我们定义了一个 IntersectionType 类型别名,表示两个匿名接口类型交叉出的类型;同时定义了一个 AgeType 类型别名,表示抽取的 ProgramLanguage age 属性的类型。

注意:类型别名,诚如其名,即我们仅仅是给类型取了一个新的名字,并不是创建了一个新的类型。

通过以上介绍,我们已经知道适用接口类型标注的地方大都可以使用类型别名进行替代,这是否意味着在相应的场景中这两者等价呢?

实际上,在大多数的情况下使用接口类型和类型别名的效果等价,但是在某些特定的场景下这两者还是存在很大区别。比如,重复定义的接口类型,它的属性会叠加,这个特性使得我们可以极其方便地对全局变量、第三方库的类型做扩展,如下代码所示:

{
interface Language {
id: number;
}

interface Language {
name: string;
}
let lang: Language = {
id: 1, // ok
name: "name", // ok
};
}

在上述代码中,先后定义的两个 Language 接口属性被叠加在了一起,此时我们可以赋值给 lang 变量一个同时包含 id 和 name 属性的对象。

不过,如果我们重复定义类型别名,如下代码所示,则会提示一个 ts(2300) 错误。

{
/** ts(2300) 重复的标志 */
type Language = {
id: number;
};

/** ts(2300) 重复的标志 */
type Language = {
name: string;
};
let lang: Language = {
id: 1,
name: "name",
};
}

类型缩减

如果将 string 原始类型和“string 字面量类型”组合成联合类型会是什么效果?效果就是类型缩减成 string 了。

同样,对于 number、boolean(其实还有枚举类型)也是一样的缩减逻辑,如下所示示例:

type URStr = "string" | string; // 类型是 string
type URNum = 2 | number; // 类型是 number
type URBoolen = true | boolean; // 类型是 boolean
enum EnumUR {
ONE,
TWO,
}
type URE = EnumUR.ONE | EnumUR; // 类型是 EnumUR

TypeScript 对这样的场景做了缩减,它把字面量类型、枚举成员类型缩减掉,只保留原始类型、枚举类型等父类型,这是合理的“优化”。

可是这个缩减,却极大地削弱了 IDE 自动提示的能力,如下代码所示:

type BorderColor = "black" | "red" | "green" | "yellow" | "blue" | string; // 类型缩减成 string

在上述代码中,我们希望 IDE 能自动提示显示注解的字符串字面量,但是因为类型被缩减成 string,所有的字符串字面量 black、red 等都无法自动提示出来了。 不要慌,TypeScript 官方其实还提供了一个黑魔法,它可以让类型缩减被控制。如下代码所示,我们只需要给父类型添加“& ”即可。

type BorderColor =
| "black"
| "red"
| "green"
| "yellow"
| "blue"
| (string & {}); // 字面类型都被保留

此时,其他字面量类型就不会被缩减掉了,在 IDE 中字符串字面量 black、red 等也就自然地可以自动提示出来了。

此外,当联合类型的成员是接口类型,如果满足其中一个接口的属性是另外一个接口属性的子集,这个属性也会类型缩减,如下代码所示:

type UnionInterce =
| {
age: "1";
}
| {
age: "1" | "2";
[key: string]: string;
};

这里因为 '1' 是 '1' | '2' 的子集,所以 age 的属性变成 '1' | '2'

{
age: 1, // 数字类型
anyProperty: 'str', // 其他不确定的属性都是字符串类型
...
}

这个问题的核心在于找到一个既是 number 的子类型,这样 age 类型缩减之后的类型就是 number;同时也是 string 的子类型,这样才能满足属性和 string 索引类型的约束关系。

never 有一个特性是它是所有类型的子类型,自然也是 number 和 string 的子类型,所以答案如下代码所示:

type UnionInterce =
| {
age: number;
}
| {
age: never;
[key: string]: string;
};
const O: UnionInterce = {
age: 2,
string: "string",
};

在上述代码中,我们在第 3 行定义了 number 类型的 age 属性,第 6 行定义了 never 类型的 age 属性,等价于 age 属性的类型是由 number 和 never 类型组成的联合类型,所以我们可以把 number 类型的值(比如说数字字面量 1)赋予 age 属性;但是不能把其他任何类型的值(比如说字符串字面量 'string' )赋予 age。

同时,我们在第 5 行第 8 行定义的接口类型中,还额外定义了 string 类型的字符串索引签名。因为 never 同时又是 string 类型的子类型,所以 age 属性的类型和字符串索引签名类型不冲突。如第 9 行第 12 行所示,我们可以把一个 age 属性是 2、string 属性是 'string' 的对象字面量赋值给 UnionInterce 类型的变量 O。