3

什么是 TypeScript

类型系统

TypeScript 是静态类型

静态类型是指编译阶段就能确定每个变量的类型,这种语言的类型错误往往会导致语法错误。TypeScript 在运行前需要先编译为 JavaScript,而在编译阶段就会进行类型检查,所以 TypeScript 是静态类型,这段 TypeScript 代码在编译阶段就会报错了:

let foo: number = 1;
foo.split(' ');
// Property 'split' does not exist on type 'number'.
// 编译时会报错(数字没有 split 方法),无法通过编译

TypeScript 是弱类型

类型系统按照「是否允许隐式类型转换」来分类,可以分为强类型和弱类型。

console.log(1 + '1');
// 打印出字符串 '11'

这段代码不管是在 JavaScript 中还是在 TypeScript 中都是可以正常运行的,运行时数字 1 会被隐式类型转换为字符串 '1',加号 + 被识别为字符串拼接,所以打印出结果是字符串 '11'


基础类型

布尔值

let isDone: boolean = false;

使用构造函数 Boolean 创造的对象不是布尔值。

let createdByNewBoolean: boolean = new Boolean(1);

// Type 'Boolean' is not assignable to type 'boolean'.
//   'boolean' is a primitive, but 'Boolean' is a wrapper object. Prefer using 'boolean' when possible.

let createdByNewBoolean: Boolean = new Boolean(1);

数字

  • 支持十进制、十六进制字面量、二进制和八进制字面量。
  • 二进制和八进制会被编译为十进制数字。

字符串

  • 可以使用双引号( ")或单引号(')表示字符串。
  • 模版字符串

元组 Tuple

元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。

// Declare a tuple type
let x: [string, number];
// Initialize it
x = ['hello', 10]; // OK
// Initialize it incorrectly
x = [10, 'hello']; // Error

当访问一个越界的元素,会使用联合类型替代

x[3] = 'world'; // OK, 字符串可以赋值给(string | number)类型

console.log(x[5].toString()); // OK, 'string' 和 'number' 都有 toString

x[6] = true; // Error, 布尔不是(string | number)类型

枚举

enum Color {Red, Green, Blue}
let c: Color = Color.Green;

默认情况下,从0开始为元素编号。 你也可以手动的指定成员的数值。 例如,我们将上面的例子改成从 1开始编号:

enum Color {Red = 1, Green, Blue}
let colorName: string = Color[2];
console.log(colorName);  // 显示'Green'因为上面代码里它的值是2

Any

  • 它提供给你一个类型系统的「后门」,TypeScript 将会把类型检查关闭。
  • 所有类型都能被赋值给any,它也能被赋值给其他任何类型。
  • 在任意值上访问任何属性都是允许的,也允许调用任何方法。
  • 变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型
let myFavoriteNumber;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;
  • 减少对any的依赖,因为需要确保类型安全。

Void

  • 表示没有任何类型。
  • 当一个函数没有返回值时,你通常会见到其返回值类型是 void
  • 声明一个void类型的变量没有什么大用,因为你只能为它赋予undefinednull

Null 和 Undefined

  • 默认情况下nullundefined是所有类型的子类型。
  • 当你指定了--strictNullChecks标记,nullundefined只能赋值给void和它们各自。
  • 可以使用联合类型string | null | undefined

Never

  • 永不存在的值的类型。
  • never类型是任何类型的子类型,也可以赋值给任何类型;
  • 没有类型是never的子类型或可以赋值给never类型(除了never本身之外)。 即使 any也不可以赋值给never
// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
    throw new Error(message);
}

数组

let list: number[] = [1, 2, 3];
let list: Array<number> = [1, 2, 3];  // 数组泛型

类数组

函数中的 arguments 实际上是一个类数组,不能用普通的数组的方式来描述,而应该用接口。
事实上常用的类数组都有自己的接口定义,如 IArguments, NodeList, HTMLCollection 等:

function sum() {
    let args: IArguments = arguments;
}
// 等价于
interface IArguments {
    [index: number]: any;
    length: number;
    callee: Function;
}

联合类型

  • 属性为多种类型之一,如字符串或者数组。使用 | 作为标记,如 string | number
  • 当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法
function getLength(something: string | number): number {
    return something.length;
}

// index.ts(2,22): error TS2339: Property 'length' does not exist on type 'string | number'.
//   Property 'length' does not exist on type 'number'.

上例中,length 不是 stringnumber 的共有属性,所以会报错。

let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
console.log(myFavoriteNumber.length); // 5
myFavoriteNumber = 7;
console.log(myFavoriteNumber.length); // 编译时报错

// index.ts(5,30): error TS2339: Property 'length' does not exist on type 'number'.

上例中,第二行的 myFavoriteNumber 被推断成了 string,访问它的 length 属性不会报错。
而第四行的 myFavoriteNumber 被推断成了 number,访问它的 length 属性时就报错了。


交叉类型

在 JavaScript 中, extend 是一种非常常见的模式,在这种模式中,可以从两个对象中创建一个新对象,新对象拥有着两个对象所有的功能。

function extend<T extends object, U extends object>(first: T, second: U): T & U {
  const result = <T & U>{};
  for (let id in first) {
    (<T>result)[id] = first[id];
  }
  for (let id in second) {
    if (!result.hasOwnProperty(id)) {
      (<U>result)[id] = second[id];
    }
  }

  return result;
}

const x = extend({ a: 'hello' }, { b: 42 });

// 现在 x 拥有了 a 属性与 b 属性
const a = x.a;
const b = x.b;

类型断言

类型断言(Type Assertion)可以用来手动指定一个值的类型。

//尖括号语法
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
//`as`语法,在TypeScript里使用JSX时,只有 `as`语法断言是被允许的。
let strLength: number = (someValue as string).length;
用途
将一个联合类型断言为其中一个类型

当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型中共有的属性或方法

interface Cat {
    name: string;
    run(): void;
}
interface Fish {
    name: string;
    swim(): void;
}

function isFish(animal: Cat | Fish) {
    if (typeof (animal as Fish).swim === 'function') {
        return true;
    }
    return false;
}
将一个父类断言为更加具体的子类

当类之间有继承关系时,类型断言也是很常见的。

interface ApiError extends Error {
    code: number;
}
interface HttpError extends Error {
    statusCode: number;
}

function isApiError(error: Error) {
    if (typeof (error as ApiError).code === 'number') {
        return true;
    }
    return false;
}
将任何一个类型断言为 any
(window as any).foo = 1;

需要注意的是,将一个变量断言为 any 可以说是解决 TypeScript 中类型问题的最后一个手段。
它极有可能掩盖了真正的类型错误,所以如果不是非常确定,就不要使用 as any

any 断言为一个具体的类型
function getCacheData(key: string): any {
    return (window as any).cache[key];
}

interface Cat {
    name: string;
    run(): void;
}

const tom = getCacheData('tom') as Cat;
tom.run();
类型断言 vs 类型声明
interface Animal {
    name: string;
}
interface Cat {
    name: string;
    run(): void;
}

const animal: Animal = {
    name: 'tom'
};
let tom = animal as Cat;
interface Animal {
    name: string;
}
interface Cat {
    name: string;
    run(): void;
}

const animal: Animal = {
    name: 'tom'
};
let tom: Cat = animal;

// index.ts:12:5 - error TS2741: Property 'run' is missing in type 'Animal' but required in type 'Cat'.

深入的讲,它们的核心区别就在于:

  • animal 断言为 Cat,只需要满足 Animal 兼容 CatCat 兼容 Animal 即可
  • animal 赋值给 tom,需要满足 Cat 兼容 Animal 才行,但是 Cat 并不兼容 Animal
类型断言 vs 泛型
function getCacheData<T>(key: string): T {
    return (window as any).cache[key];
}

interface Cat {
    name: string;
    run(): void;
}

const tom = getCacheData<Cat>('tom');
tom.run();

通过给 getCacheData 函数添加了一个泛型 <T>,我们可以更加规范的实现对 getCacheData 返回值的约束,这也同时去除掉了代码中的 any,是最优的一个解决方案。

类型别名

  • type SomeName = someValidTypeAnnotation 来创建别名
  • 与接口不同,你可以为任意的类型注解提供类型别名(在联合类型和交叉类型中比较实用)
type Text = string | { text: string };
type Coordinates = [number, number];
type Callback = (data: string) => void;

接口

Object

object表示非原始类型,也就是除numberstringbooleansymbolnullundefined之外的类型。

  • 使用接口(Interfaces)来定义对象的类型。
  • 在面向对象语言中,接口(Interfaces)是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类(classes)去实现(implement)。
  • TypeScript 中的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对「对象的形状(Shape)」进行描述。
  • 赋值的时候,变量的形状必须和接口的形状保持一致
interface SquareConfig {
  color: string;    
  width?: number;        // 可选属性,该属性可以不存在
  readonly x: number;    // 只读属性,只能在对象刚刚创建的时候修改其值
  [propName: string]: any;  //任意属性
}
  • 一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集
  • 一个接口中只能定义一个任意属性。如果接口中有多个类型的属性,则可以在任意属性中使用联合类型。[propName: string]: string | number

TypeScript具有ReadonlyArray<T>类型,可以确保数组创建后再也不能被修改。

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!   // 可以改为 a = ro as number[];
readonly vs const

做为变量使用的话用 const,若做为属性则使用readonly

函数类型

interface SearchFunc {
  (source: string, subString: string): boolean;
}

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

可索引的类型

TypeScript支持两种索引签名:字符串和数字。
当使用 number来索引时,JavaScript会将它转换成string然后再去索引对象。

interface StringArray {
  [index: number]: string;
}

let myArray: StringArray;
myArray = ["Bob", "Fred"];

let myStr: string = myArray[0];

类类型

实现接口

接口描述了类的公共部分,而不是公共和私有两部分。

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 Shape {
    color: string;
}

interface PenStroke {
    penWidth: number;
}

interface Square extends Shape, PenStroke {
    sideLength: number;
}

let square = <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 = <Counter>function (start: number) { };
    counter.interval = 123;
    counter.reset = function () { };
    return counter;
}

let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;

接口继承类

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


在引用任何一个类成员的时候都用了 this。 它表示我们访问的是类的成员。

class Animal {
    name: string;
    constructor(theName: string) { this.name = theName; }
    move(distanceInMeters: number = 0) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

class Snake extends Animal {
    constructor(name: string) { 
        super(name);       // 执行基类的构造函数,在构造函数里访问 `this`的属性之前,我们 _一定_要调用 `super()`
    }
    move(distanceInMeters = 5) {
        console.log("Slithering...");
        super.move(distanceInMeters);
    }
}

class Horse extends Animal {
    constructor(name: string) { super(name); }
    move(distanceInMeters = 45) {
        console.log("Galloping...");
        super.move(distanceInMeters);
    }
}

let sam = new Snake("Sammy the Python");
let tom: Animal = new Horse("Tommy the Palomino");

sam.move(); 
// Slithering...
// Sammy the Python moved 5m.
tom.move(34);
// Galloping...
// Tommy the Palomino moved 34m.

即使 tom被声明为 Animal类型,但因为它的值是 Horse,调用 tom.move(34)时,它会调用 Horse里重写的方法。

公共,私有与受保护的修饰符

  • 成员都默认为 public
  • private,不能在声明它的类的外部访问。
  • protected,在派生类中仍然可以访问。

    • 构造函数也可以被标记成 protected。这个类不能在包含它的类外被实例化,但是能被继承。
class Person {
    protected name: string;
    protected constructor(theName: string) { this.name = theName; }
}

// Employee 能够继承 Person
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");
let john = new Person("John"); // 错误: 'Person' 的构造函数是被保护的.

readonly修饰符

只读属性必须在声明时或构造函数里被初始化。

参数属性

参数属性可以方便地让我们在一个地方定义并初始化一个成员。

class Octopus {
    readonly name: string;
    readonly numberOfLegs: number = 8;
    constructor (theName: string) {
        this.name = theName;
    }
}

class Octopus {
    readonly numberOfLegs: number = 8;
    constructor(readonly name: string) {
    }
}

存取器

通过getters/setters来截取对对象成员的访问。

静态属性

这些属性存在于类本身上面而不是类的实例上。
每个实例想要访问这个属性的时候,都要在 origin前面加上类名。

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}));

抽象类

抽象类做为其它派生类的基类使用。 它们一般不会直接被实例化。 不同于接口,抽象类可以包含成员的实现细节。 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(); // 错误: 方法在声明的抽象类中不存在

高级技巧

构造函数

函数

可选参数

  • 可选参数必须跟在必须参数后面。
function buildName(firstName: string, lastName?: string) {
    // ...
}

默认参数

  • 默认值的参数不需要放在必须参数的后面。
  • TypeScript 会将添加了默认值的参数识别为可选参数
function buildName(firstName: string, lastName = "Smith") {
    // ...
}

剩余参数

  • 剩余参数会被当做个数不限的可选参数。 可以一个都没有,同样也可以有任意个。
function buildName(firstName: string, ...restOfName: string[]) {
  return firstName + " " + restOfName.join(" ");
}

let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");

this

interface Card {
    suit: string;
    card: number;
}
interface Deck {
    suits: string[];
    cards: number[];
    createCardPicker(this: Deck): () => Card;
}
let deck: Deck = {
    suits: ["hearts", "spades", "clubs", "diamonds"],
    cards: Array(52),
    // NOTE: The function now explicitly specifies that its callee must be of type Deck
    createCardPicker: function(this: Deck) {
        return () => {
            let pickedCard = Math.floor(Math.random() * 52);
            let pickedSuit = Math.floor(pickedCard / 13);

            return {suit: this.suits[pickedSuit], card: pickedCard % 13};
        }
    }
}

let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();

alert("card: " + pickedCard.card + " of " + pickedCard.suit);

重载

function padding(a: number, b?: number, c?: number, d?: any) {
  if (b === undefined && c === undefined && d === undefined) {
    b = c = d = a;
  } else if (c === undefined && d === undefined) {
    c = a;
    d = b;
  }
  return {
    top: a,
    right: b,
    bottom: c,
    left: d
  };
}

a,b,c,d 的值会根据传入的参数数量而变化。此函数也只需要 1 个,2 个或 4 个参数。可以使用函数重载来_强制_和_记录_这些约束。你只需多次声明函数头。最后一个函数头是在函数体内实际处于活动状态但不可用于外部。如下所示:

// 重载
function padding(all: number);
function padding(topAndBottom: number, leftAndRight: number);
function padding(top: number, right: number, bottom: number, left: number);
// Actual implementation that is a true representation of all the cases the function body needs to handle
function padding(a: number, b?: number, c?: number, d?: number) {
  if (b === undefined && c === undefined && d === undefined) {
    b = c = d = a;
  } else if (c === undefined && d === undefined) {
    c = a;
    d = b;
  }
  return {
    top: a,
    right: b,
    bottom: c,
    left: d
  };
}
padding(1); // Okay: all
padding(1, 1); // Okay: topAndBottom, leftAndRight
padding(1, 1, 1, 1); // Okay: top, right, bottom, left

padding(1, 1, 1); // Error: Not a part of the available overloads

声明文件

当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。

第三方声明文件

使用 @types 统一管理第三方库的声明文件。

npm install @types/jquery --save-dev

可以在这个页面搜索你需要的声明文件。

书写声明文件


泛型

使返回值的类型与传入参数的类型是相同的。 这里,我们使用了类型变量,它是一种特殊的变量,只用于表示类型而不是值。

function identity<T>(arg: T): T {
    return arg;
}

给identity添加了类型变量TT帮助我们捕获用户传入的类型(比如:number),之后我们就可以使用这个类型。 之后我们再次使用了 T当做返回值类型。现在我们可以知道参数类型与返回值类型是相同的了。
使用方法:

let output = identity<string>("myString");  // type of output will be 'string'
let output = identity("myString");  // type of output will be 'string'

数组:

function loggingIdentity<T>(arg: T[]): T[] {
    console.log(arg.length);  // Array has a .length, so no more error
    return arg;
}
function loggingIdentity<T>(arg: Array<T>): Array<T> {
    console.log(arg.length);  // Array has a .length, so no more error
    return arg;
}

泛型接口

interface GenericIdentityFn<T> {
    (arg: T): T;
}

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: GenericIdentityFn<number> = identity;

泛型类

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

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

泛型约束

interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);  // Now we know it has a .length property, so no more error
    return arg;
}
loggingIdentity(3);  // Error, number doesn't have a .length property

类型推论

最佳通用类型

由于最终的通用类型取自候选类型,有些时候候选类型共享相同的通用类型,但是却没有一个类型能做为所有候选类型的类型。当候选类型不能使用的时候我们需要明确的指出类型。

let zoo: Animal[] = [new Rhino(), new Elephant(), new Snake()];

如果没有找到最佳通用类型的话,类型推断的结果为联合数组类型,(Rhino | Elephant | Snake)[]

上下文类型

通常包含函数的参数,赋值表达式的右边,类型断言,对象成员和数组字面量和返回值语句。 上下文类型也会做为最佳通用类型的候选类型。

function createZoo(): Animal[] {
    return [new Rhino(), new Elephant(), new Snake()];
}
https://www.tslang.cn/docs/ha...
https://jkchao.github.io/type...
https://ts.xcatliu.com/introd...

Lebi
1 声望2 粉丝