这是我参与「第四届青训营 」笔记创作活动的第14天
「前言」
之前的文章关于类型的讲解,主要围绕着 ts 提供的基本类型,本文在这基础之上,全面的展开对 ts 类型 这一问题的深入
「类型组合」
交叉类型
交叉类型是将多个类型合并为一个类型。用 & 连接多个类型,与数学上面的 并集 同理。
interface I1 {
id: string;
show1: () => void;
name: string;
}
interface I2 {
show2: () => void
}
const obj: I1 & I2 = {
id: '01',
name: '张三',
show1() { },
show2() { }
}
这两个接口中的属性和方法在变量必须全部满足
- 如果交叉类型中的类型成员有相同的属性名,类型不同,如果至少有一个是多个简单类型,例如
number & string,最终会被视作never类型 - 如果交叉类型中的类型成员有相同的方法名,类型不同
- 返回值类型不同,
void类型会被其他类型覆盖,多个非void类型怎么继续按照交叉类型生成最终的返回值类型,参考上一条规则 - 参数列表不同,不论是参数列表的个数还是参数类型差异都会被忽略掉
- 返回值类型不同,
联合类型
联合类型表示一个值可以是几种类型之一,与交叉类型相对,使用 | 符号连接多个类型,表示 或 和 交集 的关系
interface I1 {
id: string;
show1: () => void;
name: string;
}
interface I2 {
show2: () => void
}
const obj: I1 | I2 = {
id: '01',
name: '张三',
show1() { },
show2() { }
}
如果在声明变量的时候 初始化值的成员,则表现和 联合类型 无差别,因为 ts 在这个时候不知道你所写的内容具体是什么类型的,只能兼容所有类型
正确的打开方式
const o: I2 = {
show2() { }
}
const obj: I1 | I2 = o;
「类型保护」
在之前的文章中有这么一个例子
场景:定义一个函数获取数字类型或者字符类型的长度
我们起初使用 typeof 和 类型断言 来解决的
function getLen(x: number | string) {
if (typeof x === 'number') {
return x.toString().length;
} else {
return x.length;
}
}
function getLen(x: number | string) {
if ((<string>x).length) {
return (x as string).length; // 这一行和上一行都是类型断言的写法
} else {
return x.toString().length;
}
}
第一种方式其实展示了 ts 的一种语法---类型保护,一旦我们使用这种类型保护,在之后的分支中就能明确知道类型了,然而使用 类型断言 的方式,在分支中的语句不得不多次使用类型断言约束类型。
能像 typeof 这种方式触发类型保护的方式有以下几种
类型谓词
我们只要简单地定义一个函数,它的返回值是一个 类型谓词,实现类型保护
function getLen(x: number | string) {
if (isNumber(x)) {
return x.toString().length;
} else {
return x.length;
}
}
function isNumber(x: number | string): x is number {
return true;
// 或者 return typeof x === 'number';
}
在这个例子里, x is number就是类型谓词。 谓词为 parameterName is Type这种形式, parameterName必须是来自于当前参数列表里的一个参数名。函数 返回值 必须为 布尔值
typeof
typeof类型保护只有两种形式能被识别: typeof v === "typename"和 typeof v !== "typename", "typename"必须是 "number", "string", "boolean"或 "symbol"。
如果是其他类型,会被推断为 never 类型
instanceof
instanceof类型保护 是通过构造函数来细化类型的一种方式。
与 typeof 用法一致,x instanceof Constructor,Constructor 为构造函数
更多的例子
场景:数组中的每个成员为不同类型,但有相似的属性
type IBookItem
= { author: string }
& ({
type: 'computer';
range: string;
}
| {
type: 'history';
theme: string;
})
const bookList: Array<IBookItem> = [
{
author: '罗贯中',
type: 'history',
theme: '三国演义'
},
{
author: 'ts',
type: 'computer',
range: '2001-2022',
}
]
这里我们使用了 交叉类型 和 联合类型,简化了对类型的约束,但这不是重点,看下面的例子
function logBook(book: IBookItem) {
console.log(book.author);
if (book.type === 'history') {
console.log(book.theme);
} else {
console.log(book.range);
}
}
这里的也实现了 类型保护,在判断分支的外部,我们只能访问得到 联合类型 的公共属性,在内部,经过更细致的判断,可以直接访问对应的成员了,同时也增强了 ide,拥有正确的代码自动补全
交叉类型 + 类型保护 = 自动类型推断
「关于 null」
TypeScript具有两种特殊的类型, null和 undefined,它们分别具有值null和undefined。
默认情况下,类型检查器认为 null与 undefined可以赋值给任何类型。
如果你想阻止这条规则,可以在 tsconfig.json 文件中设置 "strictNullChecks": true ,当你声明一个变量时,它不会自动地包含 null或 undefined。 你可以使用联合类型明确的包含它们
下面的代码默认你开启了 "strictNullChecks": true
带来的变化
- 可选参数和可选属性会被自动地加上
| undefined
function test(num?: number) { }
test(1);
test(undefined);
test(null); // error,类型“null”的参数不能赋给类型“number | undefined”的参数。
interface I {
num?: number;
}
const obj = <I>{};
obj.num = undefined;
obj.num = null; // 不能将类型“null”分配给类型“number | undefined”。
解决办法
- 可以在类型注解的后面手动添加
| null - 如果要使用带有
null类型,可以使用类型保护方式的限制 - 通过
!后缀去除掉null和undefined类型
interface I {
num?: number | null;
}
const obj = <I>{};
obj.num = undefined;
obj.num.toFixed(); // error
obj.num || obj.num!.toFixed(1);
「类型别名」
类型别名会给一个类型起个新名字。我们使用 type 关键字来声明一个新的类型,在之前的文章有提及过 type 简单的使用方法,这里我们讨论更高级的用法
带有泛型的类型别名
二叉树节点类型的定义
type Tree<T> = {
value: T;
left: Tree<T>;
right: Tree<T>;
}
我们也可以使用类型别名来在属性里引用自己
type Tree<T> = T & {
left: Tree<T>;
right: Tree<T>;
}
interface INode {
value: number;
}
const tree: Tree<INode> = {
value: 1,
left: null,
right: null
}
与 interface 的区别
尽管 type 可以像 interface 定义诸多类型,但是他们之间也有细微的差别
最大的区别:type 不能使用 extends 和 implements,但是 type 可以被 extends 和 implements
type INum = {
num: number;
}
interface I extends INum { }
「索引类型」
keyof 关键字
索引类型查询操作符 keyof。 对于任何类型 T, keyof T的结果为 T上已知的公共属性名的联合
class P {
private name = 'zd';
}
function test<T, K extends keyof T>(o: T, key: K): T[K] {
return o[key];
}
test(new P(), 'name') // error
索引类型和字符串索引签名
interface IObject<T> {
[keys: string]: T;
}
let keys: keyof IObject<number>; // keys 为 string | number 类型
let values: IObject<number>['foo']; // values 为 number 类型
映射类型
TypeScript 提供了从旧类型中创建新类型的一种方式 — 映射类型。 在映射类型里,新类型以相同的形式去转换旧类型里每个属性。
例如,你可以令每个属性成为 readonly类型或可选的。
type Readonly<T> = {
readonly [P in keyof T]: T[P];
}
type Partial<T> = {
[P in keyof T]?: T[P];
}
我们使用 in 关键字遍历到 keyof T 中的内容,作用类似于 vue 中 v-for="item in data"
注意:这里使用的类型被包含进了 TypeScript 的标准库
更复杂的例子(vue 中 reactive 的类型):
type Proxy<T> = {
get(): T;
set(value: T): void;
}
type Reactive<T> = {
[P in keyof T]: Proxy<T[P]>;
}
function reactive<T>(o: T): Reactive<T> {
// ... wrap proxies ...
return;
}
let proxyProps = reactive({});