Typescript学习总结
阅读原文时间:2021年06月27日阅读:1

typescript(以下简称TS)出来也有好长时间了,下面记录一下学习心得。

首先学这门语言前,请确保有以下基础知识:

  • 扎实的javascript基础知识
  • es6的基础知识
  • 面向对象编程的概念(没有也可以,就当是重新学一遍了)

接下来看一下TS的一些概念:

一、基本类型

TS的基础类型有:字符串(string)、数字(number)、布尔值(boolean)、空(null)、未定义(undefined)、数组(array)、对象(object)、元组(tuple)、枚举(enum)、any、void、never等12种。

写法为在变量后加冒号然后跟变量类型的方式,例如:

写法:

let str: string = 'str';

写法:

let num: number = 123;

写法:

let bol: boolean = false;

写法:

let n: null = null;

写法:

let u: undefined = undefined;

写法:

let arr: number[] = [1,23,4,];
let arr1: Array = [1,2,3];// 使用泛型的方式声明变量

写法:

let obj: object={};

写法:

let tuple: [number,string] = [12,'3'];

写法:

enum Num{
one=1,// 从几开始,默认为从0开始
two,// 2
three// 3
};

写法:

let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false;
let anyArr: any = [1,2,'4',false,null];

写法:

function warnUser(): void {
console.log("This is my warning message");
}
let unusable: void = undefined;
let unuse: void;

写法:

function error(message: string): never {
throw new Error(message);
}
// 推断的返回值类型为never
function fail() {
return error("Something failed");
}
// 返回never的函数必须存在无法达到的终点
function infiniteLoop(): never {
while (true) {
}
}

PS:类型断言:如果你很清楚一个变量比它现有类型更确切的类型,那么你可以使用类型断言。

类型断言有两种形式:

1.尖括号写法:

let someValue: any = "this is a string";
let strLength: number = (someValue).length;

2.As写法:

let someValueTwo: any = "this is a string";
let strLengthTwo: number = (someValueTwo as string).length;

当在TypeScript里使用JSX时,只能使用As语法断言。

2、接口

TypeScript的核心原则之一是对值所具有的结构进行类型检查。 它有时被称做“鸭式辨型法”或“结构性子类型化”。 在TypeScript里,接口的作用就是为这些类型命名定义契约。

写法:

interface 接口名 { attribute: type }

示例:

interface LabelledValue {
label: string;
}
function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}

interface SquareConfig {
color?: string;
width?: number;
}

interface Point {
readonly x: number;
readonly y: number;
}

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!
// a = ro as number[]; 用断言修改数组为可修改!

interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
return {
color: 'blue',
area:23
}
// …
}
(方法1)
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);
(方法2)索引签名
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}
(方法3)将这个对象赋值给一个另一个变量: 因为squareOptions不会经过额外属性检查
let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);

interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
let result = source.search(subString);
return result > -1;
}
// or
// mySearch = function(src: string, sub: string): boolean {
// let result = src.search(sub);
// return result > -1;
// }

TypeScript支持两种索引签名:字符串和数字。

可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。 这是因为当使用number来索引时,JavaScript会将它转换成string然后再去索引对象。 也就是说用 100(一个number)去索引等同于使用"100"(一个string)去索引,因此两者需要保持一致。

interface StringArray {
[index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0];
// 定义的StringArray接口,它具有索引签名,表示当用number去索引StringArray时会得到string类型的返回值。
interface NumberDictionary {
[index: string]: number;
length: number; // 可以,length是number类型
name: string // 错误,`name`的类型与索引类型返回值的类型不匹配
}
// 将索引签名设置为只读
interface ReadonlyStringArray {
readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error!

TypeScript也能够用它来明确的强制一个类去符合某种契约

interface ClockInterface {
currentTime: Date;
setTime(d: Date);
}
class Clock implements ClockInterface {
currentTime: Date;
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) { }
}

类静态部分与实例部分的区别

当你操作类和接口的时候,你要知道类是具有两个类型的:静态部分的类型和实例的类型。 你会注意到,当你用构造器签名去定义一个接口并试图定义一个类去实现这个接口时会得到一个错误:

interface ClockConstructor {
new (hour: number, minute: number);
}
class Clock implements ClockConstructor {
currentTime: Date;
constructor(h: number, m: number) { }
}
// 这里因为当一个类实现了一个接口时,只对其实例部分进行类型检查。 constructor存在于类的静态部分,所以不在检查的范围内。
// 因此,我们应该直接操作类的静态部分。 看下面的例子,我们定义了两个接口, ClockConstructor为构造函数所用和ClockInterface为实例方法所用。 为了方便我们定义一个构造函数 createClock,它用传入的类型创建实例。
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
interface ClockInterface {
tick();
}
function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
return new ctor(hour, minute);
}
class DigitalClock implements ClockInterface {
constructor(h: number, m: number) { }
tick() {
console.log("beep beep");
}
}
class AnalogClock implements ClockInterface {
constructor(h: number, m: number) { }
tick() {
console.log("tick tock");
}
}
let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);
// 因为createClock的第一个参数是ClockConstructor类型,在createClock(AnalogClock, 7, 32)里,会检查AnalogClock是否符合构造函数签名。

和类一样,接口也可以相互继承。 这让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里。

interface Shape {
color: string;
}
interface Square extends Shape {
sideLength: number;
}
let square = {};
square.color = "blue";
square.sideLength = 10;

继承多个接口:

interface Shape {
color: string;
}
interface PenStroke {
penWidth: number;
}
interface Square extends Shape, PenStroke {
sideLength: number;
}
let square = {};
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;

一个对象可以同时做为函数和对象使用,并带有额外的属性。

interface Counter {
(start: number): string;
interval: number;
reset(): void;
}
function getCounter(): Counter {
let counter = function (start: number) { console.log(start) };
counter.interval = 123;
counter.reset = function () { };
return counter;
}
let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;

当接口继承了一个类类型时,它会继承类的成员但不包括其实现。就好像接口声明了所有类中存在的成员,但并没有提供具体实现一样。 接口同样会继承到类的private和protected成员。 这意味着当你创建了一个接口继承了一个拥有私有或受保护的成员的类时,这个接口类型只能被这个类或其子类所实现(implement)。

当你有一个庞大的继承结构时这很有用,但要指出的是你的代码只在子类拥有特定属性时起作用。 这个子类除了继承至基类外与基类没有任何关系。 例:

class Control {
private state: any;
}
interface SelectableControl extends Control {
select(): void;
}
class Button extends Control implements SelectableControl {
select() { }
}
class TextBox extends Control {
select() { }
}
// 错误:“Image”类型缺少“state”属性。
class Image implements SelectableControl {
select() { }
}
class Location {

}
// 在上面的例子里,SelectableControl包含了Control的所有成员,包括私有成员state。 因为state是私有成员,所以只能够是Control的子类们才能实现SelectableControl接口。
因为只有 Control的子类才能够拥有一个声明于Control的私有成员state,这对私有成员的兼容性是必需的。
// 在Control类内部,是允许通过SelectableControl的实例来访问私有成员state的。 实际上, SelectableControl接口和拥有select方法的Control类是一样的。
Button和TextBox类是SelectableControl的子类(因为它们都继承自Control并有select方法),但Image和Location类并不是这样的。

3.TS类

从ECMAScript 2015,也就是ES 6开始,JavaScript程序员将能够使用基于类的面向对象的方式。

class CreateClass {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
let greeter = new CreateClass('demo');

2.1类继承:类从基类中继承了属性和方法。这里,Dog是一个派生类,它派生自ParentClass基类,通过extends关键字。派生类通常被称作子类,基类通常被称作超类。

class ParentClass {
move(distanceInMeters: number = 0) {
console.log(`Animal moved ${distanceInMeters}m.`);
}
}
class Dog extends ParentClass {
bark() {
console.log('Woof! Woof!');
}
}
const dog = new Dog();
dog.bark();
dog.move(10);
dog.bark();

2.2类私有属性:

class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}
class Rhino extends Animal {
constructor() { super("Rhino"); }
}
class Employee1 {
private name: string;
constructor(theName: string) { this.name = theName; }
}
let animal = new Animal("Goat");
let rhino = new Rhino();
let employee = new Employee1("Bob");
console.log(animal.name); // 错误

2.3类受保护属性:

class Person {
protected name: string;
constructor(name: string) { this.name = name; }
}
class Employee extends Person {
private department: string;
constructor(name: string, department: string) {
super(name)
this.department = department;
}
public getElevatorPitch() {
return `Hello, my name is ${this.name} and I work in ${this.department}.`;
}
}
let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name); // 错误

构造函数也可以被标记成 protected。 这意味着这个类不能在包含它的类外被实例化,但是能被继承。比如:

class Person2 {
protected name: string;
protected constructor(theName: string) { this.name = theName; }
}
// Employee 能够继承 Person
class Employee2 extends Person {
private department: string;
constructor(name: string, department: string) {
super(name);
this.department = department;
}
public getElevatorPitch() {
return `Hello, my name is ${this.name} and I work in ${this.department}.`;
}
}
let howard2 = new Employee2("Howard", "Sales");
let john = new Person2("John"); // 错误: 'Person' 的构造函数是被保护的.

2.4静态属性:类的静态成员,这些属性存在于类本身上面而不是类的实例上。

class Grid {
static origin = {x: 0, y: 0};
calculateDistanceFromOrigin(point: {x: number; y: number;}) {
let xDist = (point.x - Grid.origin.x);
let yDist = (point.y - Grid.origin.y);
return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
}
constructor (public scale: number) { }
}

let grid1 = new Grid(1.0); // 1x scale
let grid2 = new Grid(5.0); // 5x scale
console.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));
console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));

2.5抽象类:抽象类做为其它派生类的基类使用。 它们一般不会直接被实例化。

abstract class AbstractClass {
abstract makeSound(): void;
move(): void {
console.log('roaming the earch…');
}
}

抽象类中的抽象方法不包含具体实现且必须在派生类中实现。 抽象方法的语法与接口方法相似。 两者都是定义方法签名但不包含方法体。 然而,抽象方法必须包含 abstract关键字并且可以包含访问修饰符。

abstract class Department {
constructor(public name: string) {
}
printName(): void {
console.log('Department name: ' + this.name);
}
abstract printMeeting(): void; // 必须在派生类中实现
}
class AccountingDepartment extends Department {
constructor() {
super('Accounting and Auditing'); // 在派生类的构造函数中必须调用 super()
}
printMeeting(): void {
console.log('The Accounting Department meets each Monday at 10am.');
}
generateReports(): void {
console.log('Generating accounting reports…');
}
}
let department: Department; // 允许创建一个对抽象类型的引用
department = new Department(); // 错误: 不能创建一个抽象类的实例
department = new AccountingDepartment(); // 允许对一个抽象子类进行实例化和赋值
department.printName();
department.printMeeting();
department.generateReports(); // 错误: 方法在声明的抽象类中不存在

2.6类当做接口使用

class Point {
x: number;
y: number;
}
interface Point3d extends Point {
z: number;
}
let point3d: Point3d = {x: 1, y: 2, z: 3};

4.泛型(generic)

function identity1(arg: T): T {
return arg;
}

function identity(arg: T): T {
return arg;
}
let myIdentity: (arg: T) => T = identity; // or
let myIdentity1: {(arg: T): T} = identity;
// 这引导我们去写第一个泛型接口了。 我们把上面例子里的对象字面量拿出来做为一个接口
interface GenericIdentityFn {
(arg: T): T;
}
function identity(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn = identity;

class GenericNumber {
zeroValue: T;
add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
// GenericNumber类的使用是十分直观的,并且你可能已经注意到了,没有什么去限制它只能使用number类型。 也可以使用字符串或其它更复杂的类型。
let stringNumeric = new GenericNumber();
stringNumeric.zeroValue = "";
stringNumeric.add = function(x, y) { return x + y; };

console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));

5.枚举

enum Direction {
Up = 1, // 使用初始值,递增,否则默认从0开始
Down,
Left,
Right
}

enum Direction2 {
Up = "UP", // 每个字符串枚举成员必须进行初始化
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}

例1:

enum BooleanLikeHeterogeneousEnum {
No = 0,
Yes = "YES",
}

例2:

enum E {
Foo,
Bar,
}
function f(x: E) {
if (x !== E.Foo || x !== E.Bar) {
// ~~~
// Error! Operator '!==' cannot be applied to types 'E.Foo' and 'E.Bar'.
}
}

enum ShapeKind {
Circle,
Square,
}
interface Circle {
kind: ShapeKind.Circle;
radius: number;
}
interface Square {
kind: ShapeKind.Square;
sideLength: number;
}
let c11: Circle = {
kind: ShapeKind.Square, // 正确的为ShapeKind.Circle
// ~~~~ Error!
radius: 100,
}

枚举是在运行时真正存在的对象

enum E {
X, Y, Z
}
function f(obj: { X: number }) {
console.log('X',obj.X);
return obj.X;
}
// Works, since 'E' has a property named 'X' which is a number.
f(E);

除了创建一个以属性名做为对象成员的对象之外,数字枚举成员还具有了反向映射

enum Enum {
A
}
let a = Enum.A;
let nameOfA = Enum[a]; // "A"
// 生成的代码中,枚举类型被编译成一个对象,它包含了正向映射( name -> value)和反向映射( value -> name)。 引用枚举成员总会生成为对属性访问并且永远也不会内联代码。
// 要注意的是 不会为字符串枚举成员生成反向映射,因为枚举成员不能具有数值名,所以数字枚举成员具有反射

为了避免在额外生成的代码上的开销和额外的非直接的对枚举成员的访问,我们可以使用 const枚举。 常量枚举通过在枚举上使用 const修饰符来定义。

常量枚举注意点:

1.不会生成反向映射

2.不能直接访问值

const enum Order {
A,
B,
C,
}

外部枚举用来描述已经存在的枚举类型的形状,简单理解就是方便用户编写函数时的提示

declare enum Enum {
A = 1,
B,
C = 2
}

外部枚举和非外部枚举之间有一个重要的区别,在正常的枚举里,没有初始化方法的成员被当成常数成员。 对于非常数的外部枚举而言,没有初始化方法时被当做需要经过计算的。

用来描述一个应该存在的枚举类型的,而不是已经存在的,它的值在编译时不存在,只有等到运行时才知道。

6.模块

TypeScript 1.5里术语名已经发生了变化。 “内部模块”现在称做“命名空间”。 “外部模块”现在则简称为“模块”,这是为了与 ECMAScript 2015里的术语保持一致,(也就是说 module X { 相当于现在推荐的写法 namespace X {)。

变量,函数,类,类型别名或接口都可以通过export导出

导出声明

export interface StringValidator {
isAcceptable(s: string): boolean;
}
export const numberRegexp = /^[0-9]+$/;
export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}

导出语句

class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
export { ZipCodeValidator };
export { ZipCodeValidator as mainValidator };

7.高级类型

function extend(first: T, second: U): T & U {
let result = {};
for (let id in first) {
(result)[id] = (first)[id];
}
for (let id in second) {
if (!result.hasOwnProperty(id)) {
(result)[id] = (second)[id];
}
}
return result;
}
class Person1 {
constructor(public name: string) { }
}
interface Loggable {
log(): void;
}
class ConsoleLogger implements Loggable {
log() {
// …
return 11;
}
}
var jim = extend(new Person1("Jim"), new ConsoleLogger());
var n1 = jim.name;
jim.log();

联合类型表示一个值可以是几种类型之一。用竖线( | )分隔每个类型,所以 number | string | boolean表示一个值可以是 number, string,或 boolean。

function padLeft(value: string, padding: string | number | boolean) {
// …
}
let indentedString = padLeft("Hello world", true);
// 如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员。
interface Bird {
fly();
layEggs();
}
interface Fish {
swim();
layEggs();
}
function getSmallPet(): Fish | Bird {
// …
}
let pet = getSmallPet();
pet.layEggs(); // okay
pet.swim(); // errors

let pet = getSmallPet();
if ((pet).swim) {
(pet).swim();
}
else {
(pet).fly();
}

1.自定义类型保护

function isFish(pet: Fish | Bird): pet is Fish {
return (pet).swim !== undefined;
}
// 'swim' 和 'fly' 调用都没有问题了
if (isFish(pet)) {
pet.swim();
}
else {
pet.fly();
}

2.typeof类型保护

function isNumber(x: any): x is number {
return typeof x === "number";
}
function isString(x: any): x is string {
return typeof x === "string";
}
function padLeft(value: string, padding: string | number) {
if (isNumber(padding)) {
return Array(padding + 1).join(" ") + value;
}
if (isString(padding)) {
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}

3.instanceof类型保护

interface Padder {
getPaddingString(): string
}
class SpaceRepeatingPadder implements Padder {
constructor(private numSpaces: number) { }
getPaddingString() {
return Array(this.numSpaces + 1).join(" ");
}
}
class StringPadder implements Padder {
constructor(private value: string) { }
getPaddingString() {
return this.value;
}
}
function getRandomPadder() {
return Math.random() < 0.5 ?
new SpaceRepeatingPadder(4) :
new StringPadder(" ");
}
// 类型为SpaceRepeatingPadder | StringPadder
let padder: Padder = getRandomPadder();
if (padder instanceof SpaceRepeatingPadder) {
padder; // 类型细化为'SpaceRepeatingPadder'
}
if (padder instanceof StringPadder) {
padder; // 类型细化为'StringPadder'
}

4.可以为null的类型

let s = "foo";
s = null; // 错误, 'null'不能赋值给'string'
let sn: string | null = "bar";
sn = null; // 可以
sn = undefined; // error, 'undefined'不能赋值给'string | null'

5.可选参数和可选属性

使用了 --strictNullChecks,可选参数会被自动地加上 | undefined。

function f(x: number, y?: number) {
return x + (y || 0);
}
f(1, 2);
f(1);
f(1, undefined);
f(1, null); // error, 'null' is not assignable to 'number | undefined'
// 可选属性也会有同样的处理:
class C {
a: number;
b?: number;
}
let c = new C();
c.a = 12;
c.a = undefined; // error, 'undefined' is not assignable to 'number'
c.b = 13;
c.b = undefined; // ok
c.b = null; // error, 'null' is not assignable to 'number | undefined'

6.类型断言

可以为null的类型是通过联合类型实现,那么你需要使用类型保护来去除 null。

如果编译器不能够去除 null或 undefined,你可以使用类型断言手动去除。 语法是添加 !后缀: identifier!从 identifier的类型里去除了 null和 undefined:

function broken(name: string | null): string {
function postfix(epithet: string) {
return name.charAt(0) + '. the ' + epithet; // error, 'name' is possibly null
}
name = name || "Bob";
return postfix("great");
}
function fixed(name: string | null): string {
function postfix(epithet: string) {
return name!.charAt(0) + '. the ' + epithet; // ok
}
name = name || "Bob";
return postfix("great");
}

本例使用了嵌套函数,因为编译器无法去除嵌套函数的null(除非是立即调用的函数表达式)。 因为它无法跟踪所有对嵌套函数的调用,尤其是你将内层函数做为外层函数的返回值。 如果无法知道函数在哪里被调用,就无法知道调用时 name的类型。

7.类型别名

类型别名会给一个类型起个新名字。 类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。

type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
if (typeof n === 'string') {
return n;
}
else {
return n();
}
}
// 起别名不会新建一个类型 - 它创建了一个新名字来引用那个类型。 给原始类型起别名通常没什么用,尽管可以做为文档的一种形式使用。
// 同接口一样,类型别名也可以是泛型 - 我们可以添加类型参数并且在别名声明的右侧传入:
type Container = { value: T };
// 我们也可以使用类型别名来在属性里引用自己:
type Tree = {
value: T;
left: Tree;
right: Tree;
}
// 与交叉类型一起使用,我们可以创建出一些十分稀奇古怪的类型。
type LinkedList = T & { next: LinkedList };
interface Person {
name: string;
}
var people: LinkedList;
var s = people.name;
var s = people.next.name;
var s = people.next.next.name;
var s = people.next.next.next.name;
// 然而,类型别名不能出现在声明右侧的任何地方。
type Yikes = Array; // error

8.接口 & 类型别名

接口创建了一个新的名字,可以在其它任何地方使用。类型别名并不创建新名字—比如,错误信息就不会使用别名。 在下面的示例代码里,在编译器中将鼠标悬停在 interfaced上,显示它返回的是 Interface,但悬停在 aliased上时,显示的却是对象字面量类型。

type Alias = { num: number }
interface Interface {
num: number;
}
declare function aliased(arg: Alias): Alias;
declare function interfaced(arg: Interface): Interface;

类型别名不能被 extends和 implements(自己也不能 extends和 implements其它类型)。 因为软件中的对象应该对于扩展是开放的,但是对于修改是封闭的,你应该尽量去使用接口代替类型别名。

如果你无法通过接口来描述一个类型并且需要使用联合类型或元组类型,这时通常会使用类型别名。

9.字符串字面量类型

字符串字面量类型允许你指定字符串必须的固定值。

type Easing = "ease-in" | "ease-out" | "ease-in-out";
class UIElement {
animate(dx: number, dy: number, easing: Easing) {
if (easing === "ease-in") {
// …
}
else if (easing === "ease-out") {
}
else if (easing === "ease-in-out") {
}
else {
// error! should not pass null or undefined.
}
}
}
let button = new UIElement();
button.animate(0, 0, "ease-in");
button.animate(0, 0, "uneasy"); // error: "uneasy" is not allowed here
// 字符串字面量类型还可以用于区分函数重载:
function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
function createElement(tagName: string): Element { }

10.数字字面量类型

function rollDie(): 1 | 2 | 3 | 4 | 5 | 6 {
// …
return 1;
}
function foo(x: number) {
if (x !== 1 || x !== 2) {
// ~~~
// Operator '!==' cannot be applied to types '1' and '2'.
}
}

11.可辨识联合

你可以合并单例类型、联合类型、类型保护和类型别名来创建一个叫做【可辨识联合的高级模式】,它也称做【标签联合】或【代数数据类型】。可辨识联合在函数式编程很有用处。一些语言会自动地为你辨识联合;而TypeScript则基于已有的JavaScript模式。它具有3个要素:

interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Circle {
kind: "circle";
radius: number;
}

首先我们声明了将要联合的接口。每个接口都有kind属性但有不同的字符串字面量类型。kind属性称做可辨识的特征或标签。其它的属性则特定于各个接口。注意,目前各个接口间是没有联系的。下面我们把它们联合到一起:

type Shape = Square | Rectangle | Circle;
// 现在我们使用可辨识联合:
function area(s: Shape) {
switch (s.kind) {
case "square": return s.size * s.size;
case "rectangle": return s.height * s.width;
case "circle": return Math.PI * s.radius ** 2;
}
}

12.完整性约束

当没有涵盖所有可辨识联合的变化时,我们想让编译器可以通知我们。 比如,如果我们添加了 Triangle到 Shape,我们同时还需要更新 area:

type Shape = Square | Rectangle | Circle | Triangle;
function area(s: Shape) {
switch (s.kind) {
case "square": return s.size * s.size;
case "rectangle": return s.height * s.width;
case "circle": return Math.PI * s.radius ** 2;
}
// should error here - we didn't handle case "triangle"
}

有两种方式可以实现。

1.启用 --strictNullChecks并且指定一个返回值类型:

function area(s: Shape): number { // error: returns number | undefined
switch (s.kind) {
case "square": return s.size * s.size;
case "rectangle": return s.height * s.width;
case "circle": return Math.PI * s.radius ** 2;
}
}

因为 switch没有包涵所有情况,所以TypeScript认为这个函数有时候会返回 undefined。 如果你明确地指定了返回值类型为 number,那么你会看到一个错误,因为实际上返回值的类型为 number | undefined。 然而,这种方法存在些微妙之处且 --strictNullChecks对旧代码支持不好。

2.使用 never类型,编译器用它来进行完整性检查

function assertNever(x: never): never {
throw new Error("Unexpected object: " + x);
}
function area(s: Shape) {
switch (s.kind) {
case "square": return s.size * s.size;
case "rectangle": return s.height * s.width;
case "circle": return Math.PI * s.radius ** 2;
default: return assertNever(s); // error here if there are missing cases
}
}

这里, assertNever检查 s是否为 never类型—即为除去所有可能情况后剩下的类型。 如果你忘记了某个case,那么 s将具有一个真实的类型并且你会得到一个错误。 这种方式需要你定义一个额外的函数,但是在你忘记某个case的时候也更加明显。

13.多态的this类型

多态的this类型表示的是某个包含类或接口的子类型。这被称做F-bounded多态性。它能很容易的表现连贯接口间的继承,比如。在计算器的例子里,在每个操作之后都返回this类型:

class BasicCalculator {
public constructor(protected value: number = 0) { }
public currentValue(): number {
return this.value;
}
public add(operand: number): this {
this.value += operand;
return this;
}
public multiply(operand: number): this {
this.value *= operand;
return this;
}
// … other operations go here …
}
let v = new BasicCalculator(2)
.multiply(5)
.add(1)
.currentValue();

由于这个类使用了 this类型,你可以继承它,新的类可以直接使用之前的方法,不需要做任何的改变。

class ScientificCalculator extends BasicCalculator {
public constructor(value = 0) {
super(value);
}
public sin() {
this.value = Math.sin(this.value);
return this;
}
// … other operations go here …
}
let v = new ScientificCalculator(2)
.multiply(5)
.sin()
.add(1)
.currentValue();

如果没有this类型,ScientificCalculator就不能够在继承BasicCalculator的同时还保持接口的连贯性。multiply将会返回BasicCalculator,它并没有sin方法。然而,使用this类型,multiply会返回this,在这里就是ScientificCalculator。

14.索引类型

使用索引类型,编译器就能够检查使用了动态属性名的代码。例如,一个常见的JavaScript模式是从对象中选取属性的子集。

function pluck(o, names) {
return names.map(n => o[n]);
}

下面是如何在TypeScript里使用此函数,通过 索引类型查询和 索引访问操作符:

function pluck(o: T, names: K[]): T[K][] {
return names.map(n => o[n]);
}
interface Person {
name: string;
age: number;
}
let person: Person = {
name: 'Jarid',
age: 35
};
let strings: string[] = pluck(person, ['name']); // ok, string[]

编译器会检查 name是否真的是 Person的一个属性。 本例还引入了几个新的类型操作符。 首先是 keyof T, 索引类型查询操作符。 对于任何类型 T, keyof T的结果为 T上已知的公共属性名的联合。 例如:

let personProps: keyof Person; // 'name' | 'age'
// keyof Person是完全可以与 'name' | 'age'互相替换的。 不同的是如果你添加了其它的属性到 Person,例如 address: string,那么 keyof Person会自动变为 'name' | 'age' | 'address'。
你可以在像 pluck函数这类上下文里使用 keyof,因为在使用之前你并不清楚可能出现的属性名。 但编译器会检查你是否传入了正确的属性名给 pluck:
pluck(person, ['age', 'unknown']); // error, 'unknown' is not in 'name' | 'age'
// 第二个操作符是 T[K], 索引访问操作符。 在这里,类型语法反映了表达式语法。 这意味着 person['name']具有类型 Person['name'] — 在我们的例子里则为 string类型。
然而,就像索引类型查询一样,你可以在普通的上下文里使用 T[K],这正是它的强大所在。 你只要确保类型变量 K extends keyof T就可以了。 例如下面 getProperty函数的例子:
function getProperty(o: T, name: K): T[K] {
return o[name]; // o[name] is of type T[K]
}
// getProperty里的 o: T和 name: K,意味着 o[name]: T[K]。 当你返回 T[K]的结果,编译器会实例化键的真实类型,因此 getProperty的返回值类型会随着你需要的属性改变。
let name: string = getProperty(person, 'name');
let age: number = getProperty(person, 'age');
let unknown = getProperty(person, 'unknown'); // error, 'unknown' is not in 'name' | 'age'

15.索引类型和字符串索引签名

keyof和 T[K]与字符串索引签名进行交互。 如果你有一个带有字符串索引签名的类型,那么 keyof T会是 string。 并且 T[string]为索引签名的类型:

interface Map {
[key: string]: T;
}
let keys: keyof Map; // string
let value: Map['foo']; // number

16.映射类型

映射类型指从旧类型中创建新类型

type Readonly = {
readonly [P in keyof T]: T[P];
}
type Partial = {
[P in keyof T]?: T[P];
}
// 像下面这样使用:
type PersonPartial = Partial;
type ReadonlyPerson = Readonly;

最简单的映射类型和它的组成部分:

type Keys = 'option1' | 'option2';
type Flags = { [K in Keys]: boolean };

它的语法与索引签名的语法类型,内部使用了 for .. in。 具有三个部分:

  1. 类型变量 K,它会依次绑定到每个属性。
  2. 字符串字面量联合的 Keys,它包含了要迭代的属性名的集合。
  3. 属性的结果类型。

在个简单的例子里, Keys是硬编码的的属性名列表并且属性类型永远是 boolean,因此这个映射类型等同于:

type Flags = {
option1: boolean;
option2: boolean;
}

在真正的应用里,可能不同于上面的 Readonly或 Partial。 它们会基于一些已存在的类型,且按照一定的方式转换字段。 这就是 keyof和索引访问类型要做的事情:

type NullablePerson = { [P in keyof Person]: Person[P] | null }
type PartialPerson = { [P in keyof Person]?: Person[P] }
// 但它更有用的地方是可以有一些通用版本。
type Nullable = { [P in keyof T]: T[P] | null }
type Partial = { [P in keyof T]?: T[P] }

在这些例子里,属性列表是 keyof T且结果类型是 T[P]的变体。 这是使用通用映射类型的一个好模版。 因为这类转换是 同态的,映射只作用于 T的属性而没有其它的。 编译器知道在添加任何新属性之前可以拷贝所有存在的属性修饰符。 例如,假设 Person.name是只读的,那么 Partial.name也将是只读的且为可选的。

下面是另一个例子, T[P]被包装在 Proxy类里:

type Proxy = {
get(): T;
set(value: T): void;
}
type Proxify = {
[P in keyof T]: Proxy;
}
function proxify(o: T): Proxify {
// … wrap proxies …
}
let proxyProps = proxify(props);

注意 Readonly和 Partial用处不小,因此它们与 Pick和 Record一同被包含进了TypeScript的标准库里:

type Pick = {
[P in K]: T[P];
}
type Record = {
[P in K]: T;
}
// Readonly, Partial和 Pick是同态的,但 Record不是。 因为 Record并不需要输入类型来拷贝属性,所以它不属于同态:
type ThreeStringProps = Record<'prop1' | 'prop2' | 'prop3', string>
// 非同态类型本质上会创建新的属性,因此它们不会从它处拷贝属性修饰符。
// 由映射类型进行推断
// 现在你了解了如何包装一个类型的属性,那么接下来就是如何拆包。 其实这也非常容易:
function unproxify(t: Proxify): T {
let result = {} as T;
for (const k in t) {
result[k] = t[k].get();
}
return result;
}
let originalProps = unproxify(proxyProps);

注意这个拆包推断只适用于同态的映射类型。 如果映射类型不是同态的,那么需要给拆包函数一个明确的类型参数。

预定义的有条件类型

  • TypeScript 2.8在lib.d.ts里增加了一些预定义的有条件类型:
  • Exclude -- 从T中剔除可以赋值给U的类型。
  • Extract -- 提取T中可以赋值给U的类型。
  • NonNullable -- 从T中剔除null和undefined。
  • ReturnType -- 获取函数返回值类型。
  • InstanceType -- 获取构造函数类型的实例类型。

例如:

type T00 = Exclude<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "b" | "d"
type T01 = Extract<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "a" | "c"
type T02 = Exclude void), Function>; // string | number
type T03 = Extract void), Function>; // () => void
type T04 = NonNullable; // string | number
type T05 = NonNullable<(() => string) | string[] | null | undefined>; // (() => string) | string[]
function f1(s: string) {
return { a: 1, b: s };
}
class C {
x = 0;
y = 0;
}
type T10 = ReturnType<() => string>; // string
type T11 = ReturnType<(s: string) => void>; // void
type T12 = ReturnType<(() => T)>; // {}
type T13 = ReturnType<(() => T)>; // number[]
type T14 = ReturnType; // { a: number, b: string }
type T15 = ReturnType; // any
type T16 = ReturnType; // any
type T17 = ReturnType; // Error
type T18 = ReturnType; // Error
type T20 = InstanceType; // C
type T21 = InstanceType; // any
type T22 = InstanceType; // any
type T23 = InstanceType; // Error
type T24 = InstanceType; // Error

注意:Exclude类型是建议的Diff类型的一种实现。我们使用Exclude这个名字是为了避免破坏已经定义了Diff的代码,并且我们感觉这个名字能更好地表达类型的语义。我们没有增加Omit类型,因为它可以很容易的用Pick>来表示。