类型推论
TypeScript 里,在有些没有明确指出类型的地方,类型推论会帮助提供类型。
class Rhino {
name: string;
constructor() {
this.name = "";
}
}
class Elephant {
age: number;
constructor() {
this.age = 0;
}
}
class Snake {
job: string;
constructor() {
this.job = "";
}
}
let x = [0, 1, null]; // x: (number | null)[]
let y = Math.random() < 0.5 ? 100 : "Hello world"; // y: string | number
let z = [new Rhino(), new Elephant(), new Snake()]; // z: (Snake | Rhino | Elephant)[]
如果没有找到最佳通用类型的话,类型推断的结果为联合类型。
联合类型和类型守卫
// 联合类型
type Types = number | string
function typeFn (type: Types, input: string) {
// 假如这样定义type,则需要判断
}
如果不判断会报错,情况如下
function typeFn (type: Types, input: string) {
// 报错 运算符 + 号不能应用于 string
return new Array(type + 1).join('') + input
}
function typeFn(type: Types, input: string) {
// 类型守卫
if (typeof type === "number") return new Array(type + 1).join(" ") + input;
return type + input;
}
类型的窄化就是根据判断类型重新定义更具体的类型。
那么问题来了学这玩意干啥?JS不香吗?
个人观点:
使用 TypeScript 可以帮你降低 JavaScript 弱语言的脆弱性,帮你减少由于不正确类型导致错误产生的风险,以及各种 JavaScript 版本混杂造成错误的风险。
TypeScript 只是把高级语言的强类型这个最主要的特征引入 JavaScript,就解决了防止我们在编写 JavaScript 代码时因为数据类型的转换造成的意想不到的错误,增加了我们排查问题的困难性。
typeof的类型守卫
"string"
"number"
"bigint" // ES10新增
"boolean"
"symbol" // ES6新增
"undefined"
"object"
"function"
注意:
typeof null等于object
function strOrName (str: string | string[] | null) {
if (typeof str === 'object') {
for (const s of str) {
// 报错 因为str有可能是 null
console.log('s', s)
}
} else if (typeof str === 'string') console.log('str', str)
else console.log('')
}
真值窄化
0
NAN
""
0n // 0的 bigint 版本
null
undefined
避免 null 的错误可以利用真值窄化
function strOrName (str: string | string[] | null) {
// 利用真值判断
if (str && typeof str === 'object') {
for (const s of str) {
console.log('s', s)
}
} else if (typeof str === 'string') console.log('str', str)
else console.log('')
}
或者这样也行
function valOrName(values: number[] | undefined, filter: number): number[] | undefined {
if (!values) return values;
return values.filter((item) => item > filter);
}
小结:真值窄化帮助我们更好的处理
null / undefined / 0等值
相等性窄化
相等性窄化就是利用
===、!==、==、!=等运算符进行窄化
Example1:
function example(x: string | number, y: string | boolean) {
if (x === y) {
x.toUpperCase(); // x: string
y.toLowerCase(); // y: string
} else {
console.log(x); // x: string | number
console.log(y); // y: string | boolean
}
}
Example2:
function strOrName(str: string | string[] | null) {
if (str === null) return;
if (typeof str === "object") {
for (const s of str) {
console.log(s); // s: string
}
} else if (typeof str === "string") console.log(str); // str: string
else console.log("");
}
Example3:
interface Types {
value: number | null | undefined;
}
function valOrType(type: Types, val: number) {
// null 和 undefined 都是false,只能是number
if (type.value != null) {
type.value *= val;
}
}
in 操作符窄化
in 是检查对象中是否有属性,现在充当一个“type guard”的角色
interface A { a: number;}
interface B { b: string;}
function foo(x: A | B) {
if ("a" in x) return x.a;
return x.b;
}
is 关键字窄化
TypeScript 中 is 关键字表示是否属于某个类型,可以有效地缩小类型范围
const isString = (val: any): val is string => {
return typeof val === 'string'
}
isString 是判断传入参数是否为 String 类型的函数,用 is string 限定了返回值类型,这里估计有人会有这样的疑问:用 Boolean 类型也可以限制函数类型。Boolean 的确可以限制 isString 函数的返回类型,但是使用 is string 可以更好地缩小类型范围,避免一些隐藏的错误。
当用 Boolean 来限制 isString() 函数返回类型,以下代码不会有编译错误,运行时会报错误,因为 toExponential() 是 Number 类型的一个方法,在 String 类型上不存在。
function example (val: any) {
if (isString(val)) {
console.log(val.length)
console.log(val.toExponential(2))
}
}
// 运行时报错 Uncaught TypeError: val.toExponential is not a function
example('test')
另外,如果将 val.toExponential(2) 放在 isString 外面,也会是编译时不会出现错误,运行时会报错误。
function example(val: any) {
if (isString(val)) console.log(val.length);
console.log(val.toExponential(2));
}
// 运行时报错 Uncaught TypeError: val.toExponential is not a function
example("test");
但如果用 is String 来限定 isString() 函数返回值,此时会直接报编译错误,当然运行时肯定也会抛出错误:
function example(val: any) {
if (isString(val)) {
console.log(val.length);
// 类型“string”上不存在属性“toExponential”。
console.log(val.toExponential(2));
}
}
example("test"); // Uncaught TypeError: val.toExponential is not a function
根据上面的例子,就会理解 TypeScript 中 is 关键字可以缩小类型范围,可以帮助开发者在编辑阶段发生错误,从而避免一些隐藏的运行时错误。
项目实战中使用 filter 方法过滤未定义的值:
function isDefined<T>(x: T | undefined): x is T {
return x !== undefined;
}
const supermans = ["a", "b", "c", "d"];
const members = ["a", "b"]
.map((item) => supermans.find((n) => item === n))
.filter(isDefined);
instanceof 操作符窄化
instanceof 表达式的右侧必须属于类型 any,或属于可分配给 Function 接口类型的类型。
function dateInval(x: Date | string) {
if (x instanceof Date) console.log(x.toUTCString()); // x: Date
else console.log(x.toUpperCase()); // x: string
}
never 的妙用
Unreachable code 检查
通常来说,我们手动标记函数返回值为never类型,来帮助编译器识别「unreachable code」,并帮助我们收窄(narrow)类型。下面是一个没标记的例子:
function throwError() {
throw new Error();
}
function firstChar(msg: string | undefined) {
if (msg === undefined) throwError();
let chr = msg.charAt(1); // 报错:Object is possibly 'undefined'.
}
由于编译器不知道throwError是一个无返回的函数,所以throwError()之后的代码被认为在任意情况下都是可达的,让编译器误会msg的类型是string | undefined。
这时候如果标记上了never类型,那么msg的类型将会在空检查之后收窄为string:
function throwError(): never {
throw new Error();
}
function firstChar(msg: string | undefined) {
if (msg === undefined) throwError();
let chr = msg.charAt(1);
}
类型运算
最小因子
像Promise.race合并的多个Promise,有时是无法确切知道时序和返回结果的。现在我们使用一个Promise.race来将一个有网络请求返回值的Promise和另一个在给定时间之内就会被reject的Promise合并起来。
async function fetchNameWithTimeout(userId: string): Promise<string> {
const data = await Promise.race([
fetchData(userId),
timeout(3000)
])
return data.userName;
}
下面是一个timeout函数的实现,如果超过指定时间,将会抛出一个Error。由于它是无返回的,所以返回结果定义为了Promise<never>:
function timeout(ms: number): Promise<never> {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error("Timeout!")), ms)
})
}
很好,接下来编译器会去推断Promise.race的返回值,因为race会取最先完成的那个Promise的结果,所以在上面这个例子里,它的函数签名类似这样:
function race<A, B>(inputs: [Promise<A>, Promise<B>]): Promise<A | B>
代入fetchData和timeout进来,A则是 { userName: string },而B则是never。因此,函数输出的promise返回值类型为{ userName: string } | never。 又因为never是最小因子,可以消去。故返回值可简化为{ userName: string },这正是我们希望的。
那如果在这里使用了any或者unknown,结果又会怎样呢?
// 使用 any
function timeout(ms: number): Promise<any> {}
// { userName: string } | any => any,失去了类型检查
async function fetchNameWithTimeout(userId: string): Promise<string> {
return data.userName; // ❌ data 被推断为 any
}
any 很好理解,虽然能正常通过,但相当于没有类型检查了。
// 使用 unknown
function timeout(ms: number): Promise<unknown> {}
// { userName: string } | unknown => unknown,类型被模糊
async function fetchNameWithTimeout(userId: string): Promise<string> {
return data.userName; // ❌ data 被推断为 unknown
}
unknown则是模糊了类型,需要我们手动去收窄类型。
当我们严格使用never来描述「unreachable code」时,编译器便能够帮助我们准确地收窄类型,做到代码即文档。
条件类型中使用
我们经常在条件类型中见到never,它被用于表示else的情况。
type Arguments<T> = T extends (...args: infer A) => any ? A : never
type Return<T> = T extends (...args: any[]) => infer R ? R : never
对于上述推导函数参数和返回值的两个条件类型,即使传入的T是非函数类型,我们也能够得到编译器的提示:
// Error: Type '3' is not assignable to type 'never'
const x: Return<"not a function type"> = 3;
在收窄联合类型时,never也巧妙地发挥了它作为最小因子的作用。比如说下面这个从T中排除null和undefined的例子:
type NullOrUndefined = null | undefined
type NonNullable<T> = T extends NullOrUndefined ? never : T
// 运算过程
type NonNullable<string | null>
// 联合类型被分解成多个分支单独运算
=> (string extends NullOrUndefined ? never : string) | (nullextends NullOrUndefined ? never : null)
// 多个分支得到结果,再次联合
=> string | never
// never 在联合类型运算中被消解
=> string
Exhaustive Check
Example1:
interface Shape {
kind: "cirle" | "square",
redius?: number
sideLength?: number
}
function getAreaOne (shape: Shape) {
// 报错
return Math.PI * shape.redius * 2
}
function getAreaTwo (shape: Shape) {
// 窄化还是报错
if (shape.kind === 'cirle') return Math.PI * shape.redius * 2
}
function getAreaThree (shape: Shape) {
// 利用非空断言可以(谨慎使用非空断言)
if (shape.kind === 'cirle') return Math.PI * shape.redius! * 2
}
Example2:
interface Circle {
kind: "cirle";
redius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;
const getAreaOne = (shape: Shape) => shape.kind === "cirle" && Math.PI * shape.redius * 2;
function getAreaTwo(shape: Shape) {
switch (shape.kind) {
case "cirle":
return Math.PI * shape.redius * 2;
case "square":
return shape.sideLength * 2;
default:
const _example: never = shape;
return _example;
}
}