乐趣区

关于前端:Typescript-进阶与实践一

一、背景

在日常的开发工作中,我发现咱们的前端工程都反对 TypeScript,而团队内的同学在写代码时还是以 JavaScript 为主,而其它的一些用到了 TypeScript 的代码,有很多都是在写“AnyScript”,用到的 TypeScript 的个性很少,也没有把应用 TypeScript 所带来的长处施展进去,于是就有了这篇分享。

绝对于只应用 JavaScript 来说,应用 TypeScript 的能带来如下益处:

  1. 得益于 TypeScript 的动态类型检测,能够让局部 JavaScript 谬误在开发阶段就被发现并解决;
  2. 应用 TypeScript 能够减少代码的可读性和可维护性。在简单的大型利用中,它能让利用更易于保护、迭代,且稳固牢靠,也会让你更有安全感;
  3. TypeScript 的类型推断与 IDE 联合带来更好的代码智能提醒、重构,让开发体验和效率有了极大的晋升;

这篇文章次要介绍 TypeScript 中的一些根底个性、进阶个性的应用,并联合集体的了解和实践经验,实用于对 TypeScript 曾经有比拟根底的理解或者曾经理论用过一段时间的前端开发者,心愿能对大家有所帮忙。

二、根底

2.1 原始类型

在 JavaScript 中,原始类型指的是非对象且没有办法的数据类型,它包含  numberstringbooleannullundefinedsymbol 等。TypeScript 中内置原始类型与 JavaScript 类型的映射关系如下表所示:

JavaScript 原始类型 TypeScript 原始类型
boolean boolean
number number
string string
bigint bigint
symbol symbol
null null
undefined undefined

其用法如上面示例所示:

/* 显式类型申明 */
var num1: number = 123;
var str1: string = 'hello';

/* 也能够省略类型申明,TypeScript 能够依据值的类型推断出类型 */
var num2 = 123; // 类型为 number
var str2 = 'hello'; // 类型为 string

2.2 内置全局对象

JavaScript 中内置的装箱类型(NumberStringBoolean 等)以及其它内置对象(DateErrorArrayMapSetRegExpPromise 等)在 TypeScript 中都有其对应的同名类型。

申明类型时须要留神这一点,在能应用 numberstringboolean 等原始类型来标注类型中央就不要应用 NumberStringBoolean 等包装类的类型去标注类型,二者在 TypeScript 中不是齐全等价的,如上面示例所示:

let primitiveNumber: number = 123;
let wrappedNumber: Number = 123;

wrappedNumber = primitiveNumber;
primitiveNumber = wrappedNumber; // @error: Type 'Number' is not assignable to type 'number'. (2322)

let primitiveString: string = 'hello';
let wrappedString: String = 'hello';

wrappedString = primitiveString;
primitiveString = wrappedString; // @error: Type 'String' is not assignable to type 'string'. (2322)

在理论开发场景中,咱们简直应用不到 NumberStringBoolean 等类型,它们并没有什么非凡的用处。咱们在写 JavaScript 时,通常不会应用 NumberStringBoolean 等构造函数来 new 一个相应的实例。

2.3 字面量类型

除了原始类型 stringnumberboolean 之外,咱们还能够将类型标注为特定的字符串和数字、布尔值。将变量类型标注为字面量类型后,它的值就须要与字面量类型对应的字面量值匹配,如上面示例所示:

const num1: 123 = 123;
const num2: 123 = 1234; // @error: Type '1234' is not assignable to type '123'.(2322)

const str1: 'Hello' = 'Hello';
const str2: 'Hello' = 'Hello world!'; // @error: Type '"Hello world!"' is not assignable to type '"Hello"'.(2322)

const bool1: true = true;
const bool2: true = false; // @error: Type 'false' is not assignable to type 'true'.(2322)

2.4 联结类型 & 枚举

2.4.1 联结类型

在申明变量的类型时,咱们个别不会将它限度为某一个字面量类型,一个只能有一个值的变量并没有什么用。所以咱们个别会将字面量类型与联结类型搭配应用。

联结类型用来示意变量、参数的类型不是繁多原子类型,而可能是多种不同的类型的组合,如上面示例所示:

let color: 'blue' | 'green' | 'red';

color = 'green';
color = 'blue';
color = 'red';
color = 'yellow'; // @error: Type '"yellow"' is not assignable to type '"blue" | "green" | "red"'.(2322)

function printText(s: string, alignment: 'left' | 'right' | 'center') {// ...}

printText('Hello', 'left');
printText('Hello', 'center');
printText('Hello', 'top'); // @error: Argument of type '"top"' is not assignable to parameter of type '"left" | "right" | "center"'.

在一些场景中,TypeScript 针对联结类型做了类型放大优化,当联结的成员同时存在子类型和父类型时,类型会只保留父类型,如上面示例所示:

/* 上面的类型会放大到只保留父类型 */
type Str = 'string' | string; // 类型为 string
type Num = 2 | number; // 类型为 number
type Bool = true | boolean; // 类型为 boolean

这个优化减弱了 IDE 的主动提醒能力。在 TypeScript 官网仓库的 issue Literal String Union Autocomplete · Issue #29729 · microsoft/TypeScript 的探讨中,TypeScript 官网提供了一个小技巧来使 IDE 的主动提醒失效,如示例所示:

/* 在 IDE 中,给 color1 赋值时,不会取得提醒 */
type BorderColor = 'black' | 'red' | 'green' | 'yellow' | 'blue' | string;
const color1: BorderColor = 'black';

/* 给父类型增加“& {}”后,就能够让 IDE 的主动提醒失效 */
type BGColor = 'black' | 'red' | 'green' | 'yellow' | 'blue' | (string & {});
const color2: BGColor = 'black';

2.4.2 枚举

枚举是 TypeScript 具备的少数几个不是 JavaScript 类型级扩大的性能之一,其用法可参考 TypeScript: Handbook – Enums。它与其它的类型有些不同,枚举兼具值和类型于一体。如示例所示:

在将 JavaScript 我的项目逐渐降级到 TypeScript 时,我的项目中存在很多老的 JavaScript 代码,你能够将 TypeScript 中申明的枚举导入到 JavaScript 代码中应用。不过更举荐的做法是,应用诸如 airbnb/ts-migrate 之类的工具,疾速将我的项目的代码都转成 TypeScript,而后将类型查看从宽松逐渐过渡到严格。

常量枚举

通过增加 const 修饰符来定义常量枚举,常量枚举定义在编译为 JavaScript 之后会被抹除,能够在肯定水平上缩小编译后的 JavaScript 代码量,枚举成员的值会被间接内联到应用了枚举成员的中央,使编译后的产物构造更清晰,可读性更高。如示例所示:

2.5 数组 & 元组

2.5.1 数组

在 TypeScript 中申明数组时,能够指定数组元素的类型,如上面示例所示:

const numArr1: number[] = [1, 2, 3];
const strArr1: string[] = ['hello', 'world'];

const numArr2 = [1, 2, 3]; // 类型为 number[]
const strArr2 = ['hello', 'world']; // 类型为 string[]

你也能够用 Array<number> 之类的形式来申明数组,这个将在泛型局部讲到。这两种形式实质上并没有区别,更举荐应用 number[] 的形式来申明数组,因为这种形式代码量更少。

2.5.2 元组

根本用法

元组类型与数组类型有些类似,数组和元组转译为 JavaScript 后都是数组。它与数组类型的区别在于它能够确切的申明数组中蕴含多少个元素以及各个元素的具体类型,如上面示例所示:

type StringNumberPair = [string, number];

const tuple1: StringNumberPair = ['age', 21];
const tuple2: StringNumberPair = ['age', 21, 22]; // @error: Type '[string, number, number]' is not assignable to type 'StringNumberPair'.(2322)

React 中的 useState hook 的返回值就是一个元组,它的类型定义相似于:

(state: State) => [State, SetState];

元组还常常用于申明函数的参数的类型,如上面示例所示:

function add(...args: [number, number]) {const [x, y] = args;
  return x + y;
}
命名元组

下面示例中这种形式尽管能够申明函数的参数类型,然而没有蕴含函数的参数名信息,如果参数数量比拟多的话,这种申明形式看起来就比拟累了,于是官网在 TypeScript 4.0 中反对了给元组的成员命名,如上面示例所示:

type Tag = [name: string, value: string];

const tags: Tag[] = [['胜利', 'SUCEESS'],
  ['失败', 'FAILURE'],
];

function add(a: number, b: number) {}

// 在 4.0 时,这里获取到的参数类型为 [a: number, b: number]
type CenterMapParams = Parameters<typeof add>;

// 在 3.9 时, 类型看起来会是上面这样
type OldCenterMapParams = [number, number];

2.6 函数

2.6.1 根本用法

函数是在 JavaScript 中传递数据的次要形式,在 TypeScript 中你能够为函数的参数和返回值指定类型,如上面示例所示:

// 申明参数 a 和 b 的类型为 number
function add(a: number, b: number) {console.log(`${a} + ${b} = ${a + b}`);
}

// 申明返回值的类型为 number
function getRandom(): number {return Math.random();
}

2.6.2 函数重载

JavaScript 是一门动静语言,针对同一个函数,它能够有多种不同类型的参数与返回值。而在 TypeScript 中,也能够相应地表白不同类型的参数和返回值的函数。

函数重载须要蕴含重载签名和实现签名,重载签名的列表的各个成员必须是函数实现签名的子集,如上面示例所示:

// 重载签名
function len(s: string): number;
function len(arr: any[]): number;
// 实现签名
function len(x: any) {return x.length;}

函数实现时,TypeScript 不会限度函数实现中的返回值与重载签名严格匹配,返回值类型只须要与实现签名兼容就行。如上面的示例所示:

function reflect(str: string): string;
function reflect(num: number): number;
function reflect(strOrNum: string | number): string | number {if (typeof strOrNum === 'string') {
    // 参数为 string 类型时,参考对应的重载签名,返回值应该为 string 类型
    // 实际上你返回一个 number 也不会报错,仅须要与实现签名 string | number 兼容
    return 123456;
  } else if (typeof strOrNum === 'number') {
    // 参数为 number 类型时,参考对应的重载签名,返回值应该为 number 类型
    // 返回一个与实现签名 string | number 不兼容的类型时会报错
    return false; // @error: Type 'boolean' is not assignable to type 'string | number'.(2322)
  } else {throw new Error('Invalid param strOrNum.');
  }
}

尽管 TypeScript 容许下面这种行为,然而理论开发场景中咱们还是要防止这么写代码。

不要在更准确的重载签名之前搁置更宽泛的重载签名,TypeScript 会从上到下查找函数重载列表中与入参类型匹配的类型,并优先应用第一个匹配的重载定义。因而,放在后面函数重载签名越准确越好,如上面反例所示:

// 上面这行增加了一个宽泛的重载签名
function len(val: any): undefined;
// 重载签名
function len(s: string): number;
function len(arr: any[]): number;
// 实现签名
function len(x: any) {return x.length;}

const r1 = len(''); // 匹配上第一个重载了,类型为 undefined

如果函数仅仅只有参数个数上的区别,那么间接应用可选参数来申明就行,没有必要应用重载,如上面反例所示:

function diff(one: string): number; // 这行重载签名写或者不写是没有区别的
function diff(one: string, two?: string): number {return 0;}

更多函数类型的用法参考 Documentation – More on Functions。

2.7 对象

除了原始类型之外的其它所有类型都是对象类型。要定义对象类型,咱们只须要列出它的属性和类型。如上面示例所示:

// 一个承受对象类型参数的函数
function printCoord(pt: { x: number; y: number}) {console.log("The coordinate's x value is " + pt.x);
  console.log("The coordinate's y value is " + pt.y);
}

printCoord({x: 3, y: 7});

2.7.1 可选属性

在属性名后减少 ? 润饰即示意可选属性,如上面示例所示:

function printCoord(pt: { x: number; y?: number}) {console.log("The coordinate's x value is " + pt.x);
  console.log("The coordinate's y value is " + pt.y);
}

printCoord({x: 3});
printCoord({y: 3}); // @error: Property 'x' is missing in type ……

2.7.2 只读属性

在属性名前减少 readonly 即示意属性为只读,如上面示例所示:

function printCoord(pt: { readonly x: number; y?: number}) {pt.x = 4; // @error: Cannot assign to 'x' because it is a read-only property.(2540)
}

printCoord({x: 3});

这里 readonly 只是在 TypeScript 动态类型查看层面上将属性 x 的设置行为进行了拦挡。在 JavaScript 运行时并不会产生影响。

2.7.3 object

object 类型示意任何非原始类型(stringnumberbigintbooleansymbolnullundefined)的类型。如上面示例所示:

const val1: object = 123456; // @error: (2322)
const val2: object = 'hello'; // @error: (2322)
const val3: object = undefined; // @error: (2322)
const val4: object = null; // @error: (2322)

// 上面这些不会报错
const val5: object = {name: 'Jay'};
const val6: object = () => {};
const val7: object = [];

2.7.4 Object vs object vs {}

在进行对象类型标注时,咱们可能会将对象字面量的 {}、内置全局对象 Objectobject 混同,所以这里再总结一下应用场景:

  1. 对象的装箱类型为 Object,如章节 2.2 中提到的起因,不倡议应用装箱类型来标注类型;
  2. 当确定某个值是非原始值类型时,但又不晓得它是什么对象类型时,能够应用 object,然而更举荐应用映射类型例如 Record<keyof any, unknown>   来示意;(keyof any 会依据 tsconfig.json 中的 keyofStringsOnly 配置来决定这里的类型是 string 还是 string | number | symbol
  3. 类型 {} 能够示意任何非 null / undefined 的值,因而从类型平安的角度来说不举荐应用 {}。当你想示意一个空对象时,能够应用 Record<keyof any, never> 代替;

2.8 类型别名 & 接口类型

2.8.1 类型别名

在之前的大部分示例代码中,咱们将对象类型和联结类型间接标注在变量或者属性的类型上。间接标注尽管很不便,但如果要屡次应用雷同的类型来标注时,一处类型变动,就须要批改所有用了雷同类型标注的中央,这样会导致类型保护艰难。因而,为了更好的复用类型,咱们能够为类型取一个名称,而后在所用用到这个类型的中央标注这个类型别名,这样就能解决这个问题了。如上面示例所示:

type User = {
  id: number;
  name: string;
  age: number;
};

function getCurrentUser(): User {return { id: 1, name: 'Jay', age: 21};
}

function getUserList(): User[] {return [{ id: 1, name: 'Jay', age: 21}];
}
穿插类型

当你想把多个对象类型合并到一起的时候,你可能会把多个对象的属性从新申明成一个对象,这样还是可能会导致类型保护艰难。而穿插类型就能够解决这个问题,穿插类型次要用于组合现有的对象类型,它的用法也很简略,将 & 运算符放在两个对象类型之间即可,如上面示例所示:

type Colorful = {color: string;};

type Circle = {radius: number;};

type ColorfulCircle = Colorful & Circle;

const obj1: ColorfulCircle = {
  // @error: Property 'radius' is missing in type……
  color: 'blue',
};

const obj2: ColorfulCircle = {
  color: 'blue',
  radius: 50,
};

2.8.2 接口类型

接口类型是 TypeScript 中申明命名对象类型的另一种形式,如上面示例所示:

// 申明了 Point 接口,要求必须要有 x 和 y 两个属性,且类型为 number
interface Point {
  x: number;
  y: number;
}

function printCoord(pt: Point) {console.log("The coordinate's x value is " + pt.x);
  console.log("The coordinate's y value is " + pt.y);
}

// 传入了合乎 Point 接口形容的类型的对象
printCoord({x: 100, y: 100});

与对象类型一样,接口类型能够在属性前增加 readonly 将属性变为只读类型,也能够在属性名后增加 ? 将属性变为可选类型,如上面示例所示:

type User = {
  id: number;
  name: string;
  age: number;
};

function getCurrentUser(): User {return { id: 1, name: 'Jay', age: 21};
}

function getUserList(): User[] {return [{ id: 1, name: 'Jay', age: 21}];
}
申明合并

接口具备申明合并的个性,两个同名的接口申明会合并成一个接口申明,如上面示例所示:

interface Box {
  height: number;
  width: number;
}

interface Box {scale: number;}

let box: Box = {height: 5, width: 6, scale: 10};

下面例子中的接口申明等价于上面这段接口申明:

interface Box {
  height: number;
  width: number;
  scale: number;
}
继承

后面提到了类型别名能够通过穿插类型将多个对象类型进行组合,接口也能够应用穿插类型进行组合,如上面示例所示:

interface Colorful {color: string;}

interface Circle {radius: number;}

type ColorfulCircle = Colorful & Circle;

const obj1: ColorfulCircle = {
  // @error: Property 'radius' is missing in type……
  color: 'blue',
};

const obj2: ColorfulCircle = {
  color: 'blue',
  radius: 50,
};

除了应用穿插类型进行组合之外,还能够应用 extends 关键字进行组合,如上面示例所示:

interface Colorful {color: string;}

interface Circle {radius: number;}

interface ColorfulCircle extends Colorful, Circle {}

const obj1: ColorfulCircle = {
  // @error: Property 'radius' is missing in type……
  color: 'blue',
};

const obj2: ColorfulCircle = {
  color: 'blue',
  radius: 50,
};

2.8.3 类型别名 vs 接口类型

既然类型别名和接口类型都能够申明命名对象类型,那它们之前有哪些区别呢?又有哪些实用场景呢?

区别
  1. 类型别名能够申明原始类型、联结类型、穿插类型、元组等类型,而接口不行;
  2. 类型别名不反对申明合并,而接口反对;
  3. 类型别名次要应用穿插类型来组合对象,而接口次要应用 extends 关键字来组合对象;
  4. 在 TypeScript 4.2 版本之前,当类型查看报错时,应用接口类型在一些状况下能够取得更具体的谬误提示信息,如上面示例所示;
/* 应用接口时,错误信息中将会始终显示接口名 */

interface Mammal {name: string;}

function echoMammal(m: Mammal) {}

echoMammal({name: 12343}); // 鼠标悬停提醒谬误与类型 Mammal 无关

/* 应用类型别名时,当类型未通过解决,能够正确显示类型名称 */

type Lizard = {name: string;};

function echoLizard(l: Lizard) {}

echoLizard({name: 12345}); // 鼠标悬停提醒谬误与类型 Lizard 无关

/* 应用类型别名时,当类型通过解决时,错误信息就只会显示转换后的后果的类型,而不是类型名称 */

type Arachnid = Omit<{name: string; legs: 8}, 'legs'>;

function echoSpider(l: Arachnid) {}

echoSpider({name: 12345, legs: 8}); // 鼠标悬停提醒谬误与类型 Pick<{name: string; legs: 8;}, "name"> 无关
应用场景

Interfaces create a single flat object type that detects property conflicts, which are usually important to resolve! Intersections on the other hand just recursively merge properties, and in some cases produce never. Interfaces also display consistently better, whereas type aliases to intersections can’t be displayed in part of other intersections. Type relationships between interfaces are also cached, as opposed to intersection types as a whole. A final noteworthy difference is that when checking against a target intersection type, every constituent is checked before checking against the “effective”/”flattened” type.

—— Preferring Interfaces Over Intersections – Performance · microsoft/TypeScript Wiki

从下面这段援用咱们能够得悉:

  1. 接口会创立扁平的对象类型来检测属性是否抵触,解决这些抵触通常是很重要的。而穿插类型只是递归地合并属性,在某些状况下将会产生 never 类型;在错误信息中接口名会显示的比拟好,而穿插类型则不行。
  2. TypeScript 编译器会缓存接口间的类型关系,应用接口能取得更好的性能,特地是在我的项目比较复杂时;

联合二者的区别以及性能差别,咱们能够得出结论:接口类型更适宜用来申明对象类型,以及进行对象组合、继承;类型别名更适宜用于形容非结构化类型以及类型转换等场景。

2.9 非凡类型

any

TypeScript 中有一个非凡类型 any,它是官网提供的一个选择性绕过动态类型检测的舞弊形式。你能够在不心愿特定值导致类型查看谬误时应用它。

当一个值是 any 类型时,你能够对它进行任何操作,例如:拜访它的任何属性即便该属性可能不存在、像函数一样调用它,以及任何其它在语法上非法的货色,如示例所示:

let anything: any = {};

anything.doAnything(); // 不会提醒谬误
anything = 1; // 不会提醒谬误
anything = 'x'; // 不会提醒谬误

let num: number = anything; // 不会提醒谬误
let str: string = anything; // 不会提醒谬误

当咱们将一个基于 JavaScript 的利用革新成 TypeScript 的过程中,咱们能够借助 any 来选择性增加和疏忽对某些 JavaScript 模块的动态类型检测,直至逐渐替换掉所有的 JavaScript。或者曾经引入了短少类型注解的第三方组件库时,就能够把这些值全副注解为 any 类型。

然而从久远来看,应用 any 是一个坏习惯。如果一个 TypeScript 利用中充斥了 any,此时动态类型检测起不到作用,也就与间接应用 JavaScript 简直没有区别了。因而,除非有短缺的理由,否则该当尽量避免应用 any。在我的项目中,咱们能够在 tsconfig.json 中开启 noImplicitAny   的配置项来限度 any 的应用。

unknown

unknownany 类似,它也能够示意任何值,但在类型上比 any 更平安。咱们能够将任意类型的值赋值给 unknown,但 unknown 类型的值只能赋值给 unknownany,如上面示例所示:

let value: unknown;
let num: number = value; // @error: Type 'unknown' is not assignable to type 'number'. (2322)
let anything: any = value; // 不会提醒谬误

应用 unknown 后,TypeScript 会对它做类型检测。如果不放大类型,对 unknown 执行的任何操作都会呈现谬误。因而,对于未知类型的数据,应用 unknown 比应用 any 更好,如上面示例所示:

function fn1(value: any) {return value.toFixed(); // 不报错
}

function fn2(value: unknown) {return value.toFixed(); // @error: 'value' is of type 'unknown'. (2571)
}

function fn3(value: unknown) {if (typeof value === 'number') {value.toFixed(); // 此处 hover 提醒类型是 number,不会提醒谬误
  }
}

never

never 类型示意不携带任何类型。作为函数返回值时,意味着函数抛出异样或程序终止执行,如上面示例所示:

// 函数因为永远不会有返回值,所以它的返回值类型就是 never
function ThrowError(msg: string): never {throw Error(msg);
}

// 如果函数代码中是一个死循环,那么这个函数的返回值类型也是 never
function InfiniteLoop(): never {while (true) {}}

// 当联结类型被放大到什么类型信息都没有时
function fn1(x: string | number) {if (typeof x === 'string') {// x 的类型在这个分支被放大为 string} else if (typeof x === 'number') {// x 的类型在这个分支被放大为 number} else {x; // 类型是 'never'!}
}

never 是所有类型的子类型,它能够赋值给所有类型,反过来,除了 never 本身以外的其它类型都不能赋值给 never 类型,如示例所示。

let unreachable: never = 1; // @error: (2322)

unreachable = 'string'; // @error: (2322)
unreachable = true; // @error: (2322)

let num: number = unreachable; // ok
let str: string = unreachable; // ok
let bool: boolean = unreachable; // ok

void

TypeScript 中 void 示意没有返回值的函数。即如果函数没有返回值,那它的类型就是 void,如示例所示:

// 鼠标悬停在函数名上,显示函数的返回值类型为 void
function noop() {return;}

咱们能够把 undefined 值或类型是 undefined 的变量赋值给 void 类型的变量,反过来,类型是 void 但值是 undefined 的变量不能赋值给 undefined 类型,如示例所示:

const userInfo: {id?: number} = {};

let undefinedType: undefined = undefined;
let voidType: void = undefined;

voidType = undefinedType; // ok
undefinedType = voidType; // @error: Type 'void' is not assignable to type 'undefined'. (2322)

function fn1(): void {return undefined;}

function fn2(): undefined {
  const result: void = undefined;
  return result; // @error: Type 'void' is not assignable to type 'undefined'. (2322)
}

在给函数标注返回值类型时,返回值的类型应该仅为 void 或者为其它类型,不举荐将 void 与其它类型进行联结,如示例所示:

// 什么值都不会返回时应用 void
function fn1(): void {// ……}

// 反例:函数某些状况下会有返回值,尽管类型查看能通过,但不举荐这么写。function fn3(val: unknown): number | string | void {if (typeof val === 'number' || typeof val === 'string') {return val;}
}

// 改良:将 void 替换成 undefined。function fn2(val: unknown): number | string | undefined {if (typeof val === 'number' || typeof val === 'string') {return val;}

  return;
}

2.10 类型断言

2.10.1 根本用法

当你晓得某个值的类型信息,然而 TypeScript 不晓得,就能够应用类型断言。如上面示例所示:

const foo = {};
foo.bar = 123; // Error: 'bar' 属性不存在于‘{}’foo.bas = 'hello'; // Error: 'bas' 属性不存在于 '{}'

这段代码收回了谬误正告,因为 foo 的类型推断为 {},即没有属性的对象。因而,你不能在它的属性上增加 barbas,你能够通过类型断言来防止此问题,如上面示例所示:

interface Foo {
  bar: number;
  bas: string;
}

const foo = {} as Foo; // 类型为 Foo

foo.bar = 123;
foo.bas = 'hello';

/**
 * 上面的这种形式与下面的没有任何区别,然而因为尖括号格局会
 * 与 JSX 产生语法抵触,因而更举荐应用 as 语法。*/
const foo1 = <Foo>{}; // 类型为 Foo

类型断言仅容许将类型断言为一个更具体的或者不太具体的类型,即仅在父子、子父类型之间能够应用类型断言进行转换。如上面示例所示;

/**
 * 断言成更具体的类型
 */
function fn1(event: Event) {const mouseEvent = event as MouseEvent;}

/**
 * 断言成不那么具体的类型
 */
function fn2(event: MouseEvent) {const mouseEvent = event as Event;}

/**
 * 断言成不可能的类型
 */
function fn3(event: Event) {const element = event as HTMLElement; // Error: 'Event' 和 'HTMLElement' 中的任何一个都不能赋值给另外一个}

2.10.2 双重断言

应用双重断言能够将任何一个类型断言为任何另一个类型,如上面示例所示,然而因为这种做法可能导致运行时谬误,所以 不举荐 这么做。

const num = 123 as any as string; // 类型为 string

const str = 'hello' as unknown as number; // 类型为 number

function fn3(event: Event) {const element = event as unknown as HTMLElement; // 类型为 HTMLElement}

2.10.3 非空断言

在值(变量、属性)的后边增加 ! 断言操作符,它能够用来排除值为 nullundefined 的状况,如上面示例所示:

let mayNullOrUndefinedOrString: null | undefined | string;

mayNullOrUndefinedOrString!.toString(); // ok
mayNullOrUndefinedOrString.toString(); // @error: 'mayNullOrUndefinedOrString' is possibly 'null' or 'undefined'.(18049)

2.10.4 常量断言

应用 字面量值 + as const 语法结构能够进行常量断言,对数据进行常量断言后,它的类型就变成了字面量类型且它的值不能再被批改,如上面示例所示:

let str = 'Hello world!' as const; // 类型为 "Hello world"
str = '123'; // Error: 2322

const readOnlyArr = [0, 1] as const; // 类型为 readonly [0, 1]
readOnlyArr[1] = 123; // Error: 2540

2.11 控制流剖析

JavaScript 文件中代码流动形式会影响整个程序的类型。让咱们来看一个例子:

const users = [{name: 'Ahmed'}, {name: 'Gemma'}, {name: 'Jon'}]; // users 类型为 {name: string}[]
const jon = users.find(u => u.name === 'jon');

在这个例子中,find 可能会失败,因为名字叫“jon”的用户并不一定存在,因而变量 jon 的类型为 {name: string} | undefined。而当你把鼠标悬停在上面代码示例所示的三处 jon 上时,你将会看到类型如何依据 jon 所在的地位而变动:

if (jon) {// 类型为 { name: string} | undefined
  jon; // 类型为 {name: string}
} else {jon; // 类型为 undefined}

像下面这种基于可达性的代码剖析称为控制流剖析,TypeScript 在遇到类型爱护和赋值时应用这种流剖析来放大类型。当剖析变量时,控制流能够一次又一次地拆散和从新合并,并且能够察看到该变量在每个地位具备的不同类型。

另一个控制流剖析的例子:

interface User {
  id: string;
  name: string;
  age: number;
}

type Action = {type: 'add'; user: User} | {type: 'delete'; id: string};

function addUser(user: User) {}
function deleteUser(id: string) {}

function reducer(action: Action) {switch (action.type) {
    case 'add':
      addUser(action.user); // action 是 "{type:'add', user: User}" 类型
      break;
    case 'delete':
      deleteUser(action.id); // action 是 "{type:'delete', id: string}" 类型
      break;
    default:
      throw new Error('Invalid action.');
  }
}

下面这段代码中,联结类型 Action 中的对象成员都具备属性 type,TypeScript 通过剖析 switch 语句,就能够在对应的 case 分支中将类型放大。

2.12 类型守卫

类型守卫是指通过代码来影响代码流剖析。TypeScript 能够应用现有的 JavaScript 行为在运行时对值进行验证以影响代码流。

JavaScript 中的一种常见模式是应用 typeofinstanceof 在运行时查看表达式的类型。TypeScript 能够了解这些条件,并在 if 代码块中应用时会相应地更改类型推断,如上面示例所示:

let x: unknown;

// 应用 typeof 类型守卫
if (typeof x === 'string') {x.substring(1);
  x.subtr(2); // @error: Property 'subtr' does not exist on type 'string'. Did you mean 'substr'?(2551)
}

if (x instanceof Array) {x.split(''); // @error: Property'split'does not exist on type'any[]'.(2339)
  x.forEach(item => {console.log(item);
  });
}

除了 typeofinstanceof 之外,你还能够应用 in、类型谓词来做实现类型守卫,如上面示例所示:

type Fish = {swim: () => void };
type Bird = {fly: () => void };

/* 应用 in 运算符放大类型 */

function move1(animal: Fish | Bird) {if ('swim' in animal) {return animal.swim(); // 鼠标悬停在 animal 上提醒类型为 Fish
  }

  return animal.fly(); // 鼠标悬停在 animal 上提醒类型为 Bird}

/* 应用类型谓词实现类型守卫 */

function isFish(pet: Fish | Bird): pet is Fish {return (pet as Fish).swim !== undefined;
}

function move2(animal: Fish | Bird) {if (isFish(animal)) {return animal.swim(); // 鼠标悬停在 animal 上提醒类型为 Fish
  }

  return animal.fly(); // 鼠标悬停在 animal 上提醒类型为 Bird}

三、未完待续

因为工夫和精力有限,第一局部内容就分享到这里。前面还会给大家带来 TypeScript 泛型、类型编程等内容,敬请期待。

退出移动版