TypeScript中的高级类型
类型别名 type
现在我们有这样一个代码,如果要再声明一个同样类型的对象,我们需要再重复声明一次类型。
我们应该尽可能复用我们的代码。
let man: {
readonly name: string;
age: number;
retire: (date: Date) => void;
} = {
age: 22,
name: "kevin",
retire: (date: Date) => {
console.log(date);
},
};
所以我们使用了type
关键字
type Man = {
readonly name: string;
age: number;
retire: (date: Date) => void;
};
let man: Man = {
age: 22,
name: "kevin",
retire: (date: Date) => {
console.log(date);
},
};
当然,你也可以使用接口interface
关键字,这个关键字我们会在后面讲。
联合类型|
我们可以使用联合类型,来声明一个变量的类型为多个类型的子集。
使用|
来声明联合类型:
但是,这时候,我们发现使用 ts 的代码补全时,只有 number 和 string 类型的共同的方法。
所以,我们要想办法将类型范围“缩小”:
可以看到:现在的代码补全就是对应类型的方法了
我们再查看编译出的 js 代码:
"use strict";
const func = (param) => {
if (typeof param === "number") return param;
else return parseInt(param) * 1.2;
};
可以看到,我们所以联合类型只是 ts 编译器对变量进行的类型检查
交叉类型&
我们可以使用&
来表示同时满足多个类型,也许你会想到这样声明一行代码:
let number_string: number & string;
但这是不合理的,因为没有值能够既是 number 类型又是 string 类型
我们通常用它来声明一个类型:
type Draggable = {
drag: () => void;
};
type Resizable = {
resize: () => void;
};
type UIWidget = Draggable & Resizable;
let textBox: UIWidget = {
drag: () => {},
resize: () => {},
};
字面量类型
假设我们有一个变量为quantity
let quantity: number;
这个变量的类型为 number,也就是说我们可以赋值任何数值给这个变量。
但也许我们就只想给它赋值固定的数值?
ts 允许我们使用字面量来声明类型,如 string,number,boolean 类型的值
let quantity: 50 = 100; //报错:不能将类型“100”分配给类型“50”。ts(2322)
或许你觉得这没有什么用,但当我们使用联合类型时,或许就有用了:
let quantity: 50 | 100 = 100;
当然,我们应该这样写:
type Quantity = 50 | 100;
let quantity: Quantity = 100;
对于字符串和布尔类型:
type Metric = "cm" | "m";
type Bools = true | false;
null 类型
只有 null 为 null 类型,只有 undefined 为 undefined 类型
假如我们有一个函数:
const greet = (name: string) => {
console.log(name.toUpperCase());
};
greet(null); //报错:类型“null”的参数不能赋给类型“string”的参数。ts(2345)
如果你想去除这个报错,你可以(不推荐)在 tscofig.json 中找到"strictNullChecks": true,
打开并该为 false
或许你真的想传入 null 类型,你可以声明联合类型:
当然,这里你不能光声明类型,因为在你使用name.toUpperCase()
时它会报错:name 可能为 null。
const greet = (name: string | null | undefined) => {
if (name) console.log(name.toUpperCase());
else console.log("hello");
};
greet(null);
greet(undefined);
可选链/可选属性访问符?
现在假定我们有一个函数,用来输出客户的生日,或许你不知道客户的名字。我们将 name 的类型定义为与 null 和 undefined 联合的类型
type Customer = {
birthday: Date;
};
function getCustomer(id: number): Customer | null | undefined {
return id === 0 ? null : { birthday: new Date() };
}
let customer = getCustomer(0);
if (customer !== null && customer !== undefined) console.log(customer.birthday);
在 ts 中,我们可以通过可选链/可选属性访问符(在.
前使用?
),当这个方法/属性被定义时我们调用,如果为null
或者undefined
ts 会返回undefined
,并且有短路运算
的特性。
type Customer = {
birthday: Date;
};
function getCustomer(id: number): Customer | null | undefined {
return id === 0 ? null : { birthday: new Date() };
}
let customer = getCustomer(0);
console.log(customer?.birthday); //undefined
现在我们让Customer
类型中的birthday
属性为可选,然后链式调用 getFullYear 方法。注意加上可选属性访问符?
。因为这时的customer?.birthday
可能为 null 或者 undefined:
type Customer = {
birthday?: Date;
};
function getCustomer(id: number): Customer | null | undefined {
return id === 0 ? null : { birthday: new Date() };
}
let customer = getCustomer(0);
console.log(customer?.birthday?.getFullYear()); //注意getFullYear()前的可选属性访问符
同时,我们也可以在数组和调用方法中使用:
function tryGetFirstElement<T>(arr?: T[]) {
return arr?.[0];
// equivalent to
// return (arr === null || arr === undefined) ?
// undefined :
// arr[0];
}
async function makeRequest(url: string, log?: (msg: string) => void) {
log?.(`Request started at ${new Date().toISOString()}`);
// roughly equivalent to
// if (log != null) {
// log(`Request started at ${new Date().toISOString()}`);
// }
const result = (await fetch(url)).json();
log?.(`Request finished at at ${new Date().toISOString()}`);
return result;
}
无效合并/Nullish Coalescing??
假如我们有这样的代码:
let speed: number | null = null;
let ride = {
speed: speed ? speed !== null : 30,
};
在 ts 中,我们可以使用无效合并来简化代码:
let speed: number | null = null;
let ride = {
speed: speed ?? 30,
};
当处理 null
或者 undefined
时,它可以作为一种「倒退」到默认值的方式
类型断言
有些时候,我们比 ts 编译器更清楚变量/常量的类型
let phone = document.getElementById("phone"); //let phone: HTMLElement | null
假设我们真的有一个元素的 id 为 phone,那么变量 phone 的类型就应该为 HTMLElement 而不是 HTMLElement | null
并且前面的经验也告诉我们这不利于代码补全。
我们可以使用as
关键字
let phone = document.getElementById("phone") as HTMLElement;
并且这时候的代码推断的提示也是 HTMLElement 类型的方法。
当然,我们也可以使用尖括号来进行断言<>
let phone = <HTMLElement>document.getElementById("phone");
unknown 比 any 更安全
之前我们已经讲过,any 虽然会让我们避免报错,但大量的使用 any 会让我们失去使用 ts 的意义。同样的 ts 也不鼓励我们使用 any,而是推荐我们使用 unknown,意思是”不知道是什么类型“。
不过我们直接使用 unknown 时会报错:
function reder(document: unknown) {
document.toUpperCase(); //报错:对象的类型为 "unknown"。ts(2571)
}
这是 unknown
类型的主要价值主张:TypeScript 不允许我们对类型为 unknown
的值执行任意操作。相反,我们必须首先执行某种类型检查以缩小我们正在使用的值的类型范围。
像前面提到的那样,我们进行类型范围的缩小:
function reder(document: unknown) {
if (typeof document === "string") document.toUpperCase();
}
但 typeof 只能用于基本类型,而像自定义类型,我们需要用到instanceof
关键字
class Behavior {
move() {}
}
function reder(document: unknown) {
if (document instanceof Behavior) document.move();
}
never 类型
never 代表永远不会发生的类型。它是 TypeScript 中的底层类型。它自然被分配的一些例子:
- 一个从来不会有返回值的函数(如:如果函数内含有
while(true) {}
); - 一个总是会抛出错误的函数(如:
function foo() { throw new Error('Not Implemented') }
,foo
的返回类型是never
);
例如:
function processEvents(): never {
while (true) {}
}
processEvents();
console.log("never do"); //这行代码在vscode中会呈灰色,如果不使用never类型,这会看上去能够执行
我们还可以在 tsconfig.json 中找到"allowUnreachableCode": true,
打开并改为false
这表示我们不允许不能到达的代码
现在我们得到了一个提示,这对我们是很有用的:
这也是为什么我们要使用 never。
同样的:
let bar: (err: string) => never = (err: string) => {
throw new Error(err + ":Throw my hands in the air like I just dont care");
};
bar("...");
console.log("never do"); //检测到无法访问的代码。ts(7027)