Type Script学习笔记:基础

原始数据类型

boolean

使用构造函数Boolean创建的对象不是布尔值,而是一个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.

直接调用Boolean可以返回一个boolean对象。

let createdByBoolean: boolean = Boolean(1);

在 TypeScript 中,boolean 是 JavaScript 中的基本类型,而 Boolean 是 JavaScript 中的构造函数。其他基本类型(除了 null 和 undefined)一样,不再赘述。

数值

定义数值类型的时候可以使用0x,0b,0o语法,0b和0o会被编译成十进制。

let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
// ES6 中的二进制表示法
let binaryLiteral: number = 0b1010;
// ES6 中的八进制表示法
let octalLiteral: number = 0o744;
let notANumber: number = NaN;
let infinityNumber: number = Infinity;

编译得到

var decLiteral = 6;
var hexLiteral = 0xf00d;
// ES6 中的二进制表示法
var binaryLiteral = 10;
// ES6 中的八进制表示法
var octalLiteral = 484;
var notANumber = NaN;
var infinityNumber = Infinity;

实际上在本机器测试的时候会自动换为ES6语法,不会修改对应成十进制。

string

注意模板字符串的用法,必须用反引号`包裹。

let myName: string = 'Tom';
let myAge: number = 25;

// 模板字符串
let sentence: string = `Hello, my name is ${myName}.
I'll be ${myAge + 1} years old next month.`;

console.log(sentence)

空值

js中没有空值(Void)的概念,TS中可以用void表示没有任何返回值的函数。

function alertName(): void {
    alert('My name is Tom');
}

声明一个 void 类型的变量没有什么用,因为你只能将它赋值为 undefined 和 null(只在 –strictNullChecks 未指定时):

let unusable: void = undefined;

Null 和 Undefined

与 void 的区别是,undefined 和 null 是所有类型的子类型。也就是说 undefined 类型的变量,可以赋值给 number 类型的变量

任意值

any类型允许被赋值为任意类型

let myFavoriteNumber: any = 'seven';
myFavoriteNumber = 7;

在任意值上访问任何属性,调用任何方法都是允许的,对其进行的任何操作返回的值都是任意值

未声明类型的变量

不声明的话默认是any

联合类型

取值可以是多种类型中的一种

let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;

当 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'.
function getString(something: string | number): string {
    return something.toString();
}

接口

在面向对象语言中,接口(Interfaces)是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类(classes)去实现(implement)。

TypeScript 中的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对「对象的形状(Shape)」进行描述。

interface Person {
    name: string;
    age: number;
}

let tom: Person = {
    name: 'Tom',
    age: 25
};

赋值的时候,变量的形状必须和接口的形状保持一致

有时我们希望不要完全匹配一个形状,那么可以用可选属性:

interface Person {
    name: string;
    age?: number;
}

let tom: Person = {
    name: 'Tom'
};

有时候我们希望一个接口允许有任意的属性,可以使用如下方式:

interface Person {
    name: string;
    age?: number;
    [propName: string]: any;
}

let tom: Person = {
    name: 'Tom',
    gender: 'male'
};

需要注意的是,一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集,如下的代码会报错。

interface Person {
    name: string;
    age?: number;
    [propName: string]: string;
}

let tom: Person = {
    name: 'Tom',
    age: 25,
    gender: 'male'
};

// index.ts(3,5): error TS2411: Property 'age' of type 'number' is not assignable to string index type 'string'.
// index.ts(7,5): error TS2322: Type '{ [x: string]: string | number; name: string; age: number; gender: string; }' is not assignable to type 'Person'.
//   Index signatures are incompatible.
//     Type 'string | number' is not assignable to type 'string'.
//       Type 'number' is not assignable to type 'string'.

一个接口中只能定义一个任意属性。如果接口中有多个类型的属性,则可以在任意属性中使用联合类型:

interface Person {
    name: string;
    age?: number;
    [propName: string]: string | number;
}

let tom: Person = {
    name: 'Tom',
    age: 25,
    gender: 'male'
};

有时候我们希望对象中的一些字段只能在创建的时候被赋值,那么可以用 readonly 定义只读属性,下述代码会报错

interface Person {
    readonly id: number;
    name: string;
    age?: number;
    [propName: string]: any;
}

let tom: Person = {
    id: 89757,
    name: 'Tom',
    gender: 'male'
};

tom.id = 9527;

// index.ts(14,5): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.

注意,只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候

数组

最简单的方法是使用“类型 + 方括号”来表示数组,项中不允许出现其他类型,方法也会被限制。

let fibonacci: number[] = [1, 1, 2, 3, 5];

也可以使用数组泛型(Array Generic) Array<elemType> 来表示数组:

let fibonacci: Array<number> = [1, 1, 2, 3, 5];

也可以用接口表示数组

interface NumberArray {
    [index: number]: number;
}
let fibonacci: NumberArray = [1, 1, 2, 3, 5];

其表示索引的类型是数字时值的类型是数字。

一般不用这个方法来表示普通数组,常用于表示类数组。

function sum() {
    let args: {
        [index: number]: number;
        length: number;
        callee: Function;
    } = arguments;
}

在上面的例子中,我们除了约束当索引的类型是数字时,值的类型必须是数字之外,也约束了它还有 length 和 callee 两个属性。

事实上常用的类数组都有自己的接口定义,如 IArguments, NodeList, HTMLCollection 等:

function sum() {
    let args: IArguments = arguments;
}

一个比较常见的做法是,用 any 表示数组中允许出现任意类型:

let list: any[] = ['google', 25, { website: 'http://google.com' }];

函数

// 函数声明(Function Declaration)
function sum(x: number, y: number): number {
    return x + y;
}

// 函数表达式(Function Expression) 类似于闭包
let mySum = function (x: number, y: number): number {
    return x + y;
};

上述例子中的函数表达式写法其实省略了mySum的类型,其实际类型是(x: number, y: number) => number

可以使用接口的方式来定义一个函数需要符合的形状

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

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
    return source.search(subString) !== -1;
}

用?定义可选的参数,可选参数后不允许再添加必须参数

function buildName(firstName: string, lastName?: string) {
    if (lastName) {
        return firstName + ' ' + lastName;
    } else {
        return firstName;
    }
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');

添加了默认值的参数会被识别为可选参数,此时不受可选参数后不允许再添加必须参数的限制了。

function buildName(firstName: string = 'Tom', lastName: string) {
    return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let cat = buildName(undefined, 'Cat');

可以用...rest的方式获取函数中的剩余参数

function push(array: any[], ...items: any[]) {
    items.forEach(function(item) {
        array.push(item);
    });
}

let a = [];
push(a, 1, 2, 3);

重载允许一个函数接受不同数量或类型的参数时,作出不同的处理。

function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string | void {
    if (typeof x === 'number') {
        return Number(x.toString().split('').reverse().join(''));
    } else if (typeof x === 'string') {
        return x.split('').reverse().join('');
    }
}

类型断言

值 as 类型<类型>值的方式,一般用前者,后者方括号格式本身用于表示ReactNode和泛型,容易混淆。

类型断言能够将一个联合类型断言为其中一个类型,从而调用类型独有的属性和方法。

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

如果不加以检查地使用类型断言会导致运行时错误。

类型断言能够将一个父类断言为更加具体的子类。

class ApiError extends Error {
    code: number = 0;
}
class HttpError extends Error {
    statusCode: number = 200;
}

function isApiError(error: Error) {
    if (typeof (error as ApiError).code === 'number') {
        return true;
    }
    return false;
}

虽然上述例子使用error instance of ApiError更恰当,但是如果是接口就不能使用instance of了。

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断言为具体类型从而提高代码的可维护性。一旦可以确定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();

断言的实际限制

若 A 兼容 B,那么 A 能够被断言为 B,B 也能被断言为 A

TS作为类型结构系统,其实际上比较的是最终的结构而不是定义时的关系。

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

let tom: Cat = {
    name: 'Tom',
    run: () => { console.log('run') }
};
let animal: Animal = tom;

使用双重断言可以实现强制的类型转换,但是一般来说没意义。

类型断言只会影响 TypeScript 编译时的类型,类型断言语句在编译结果中会被删除

所以类型断言不是类型转换,它不会真的影响到变量的类型。

若要进行类型转换,需要直接调用类型转换的方法

function toBoolean(something: any): boolean {
    return Boolean(something);
}

toBoolean(1);
// 返回值为 true

类型的声明是比类型断言更加严格的,只要A兼容B那么两者之间可以互相进行断言,但是只能将B赋值给A而不能将A赋值给B,不能将父类的实例赋值给类型为子类的变量。

可以用泛型实现更加规范的返回值约束。

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

声明文件

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

假如我们想使用第三方库 jQuery,一种常见的方式是在 html 中通过 <script> 标签引入 jQuery,然后就可以使用全局变量 $ 或 jQuery 了。

但是在 ts 中,编译器并不知道 $ 或 jQuery 是什么东西,因此我们需要使用declare var来定义其类型。

declare var jQuery: (selector: string) => any;

jQuery('#foo');

通常我们会将声明的语句放进单独的以.d.ts为文件名的文件,会自动解析

可以直接用在社区中获取想要的的声明文件,可以使用@types统一管理

npm install @types/jquery --save-dev

目前的npm在安装包本体如果有声明文件则会直接安装,不需要单独下载@types包了

如果我们需要书写声明文件的话方法比较复杂,需要的时候再学吧 https://ts.xcatliu.com/basics/declaration-files.html#%E4%B9%A6%E5%86%99%E5%A3%B0%E6%98%8E%E6%96%87%E4%BB%B6

内置对象

ECMAScript的内置对象有Boolean、Error、Date、RegExp 等。

let b: Boolean = new Boolean(1);
let e: Error = new Error('Error occurred');
let d: Date = new Date();
let r: RegExp = /[a-z]/;

可以参考https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects

DOM 和 BOM 提供的内置对象有:Document、HTMLElement、Event、NodeList 等。

let body: HTMLElement = document.body;
let allDiv: NodeList = document.querySelectorAll('div');
document.addEventListener('click', function(e: MouseEvent) {
  // Do something
});

TypeScript 核心库的定义文件中定义了所有浏览器环境需要用到的类型,并且是预置在 TypeScript 中的。

Node.js 不是内置对象的一部分,如果想用 TypeScript 写 Node.js,则需要引入第三方声明文件:

npm install @types/node --save-dev