TypeScript入门(一)

参考TypeScript 入门教程http://ts.xcatliu.com/

TypeScript 的特性

类型系统

  1. 按照「类型检查的时机」分类
  • 动态类型(JavaScript)
  • 静态类型(TypeScript)
  • 注:TypeScript强大的类型推论可以使大部分 JavaScript 代码经过少量的修改(或者完全不用修改)就变成 TypeScript 代码。
  1. 按照「是否允许隐式类型转换」分类
  • 强类型(Python)
  • 弱类型(JavaScript,TypeScript)

适用于任何规模

  1. 易于维护--适用于大型项目
  2. 有类型推论--同样适用于小项目
  3. 通过类型声明文件渐进式迁移至typeScript

与标准同步发展

与 ECMAScript 标准同步发展,一个语法进入到 Stage 3 阶段后,TypeScript 就会实现它。

原始基础数据类型

  1. 数值(number)
  2. 布尔值(boolean)
  3. 字符串(string)
  4. 空值(void): 用 void 表示没有任何返回值的函数
  5. null
  6. undefined
  7. symbol(es6)
  8. bigInt(es10)

使用let声明变量,const声明常量(阻止变量被重新赋值)

// 数字,支持二、八、十、十六进制 
let num: number = 3; 
let notANumber: number = NaN;
let infinityNumber: number = Infinity;

// 布尔
let isDone: boolean = false;

// 字符串,支持模板字符串
let name: strng = 'lilili'; 

// 空值:用 void 表示没有任何返回值的函数
function alertName(): void {
    alert('My name is Tom');
}

// null和undefined
let u: undefined = undefined;
let n: null = null;

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

let unusable: void = undefined;

与 void 的区别是,undefined 和 null 是所有类型的子类型,以下代码不会报错

let num: number = undefined;

let u: undefined;
let num: number = u;

Any

任意值(Any)用来表示允许赋值为任意类型。声明一个变量为任意值之后,对它的任何操作,返回的内容的类型都是任意值 变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型

let num: number = 3;
num = 'str'; // 不允许

let u: any = 5;
u = 'test' // 允许

联合类型(Union Types)

取值可以为多种类型中的一种。使用|分割每个类型。 当不确定联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型中的公共属性或方法。 联合类型在被赋值的时候会根据类型推论规则推出一个类型。

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

// 访问公共属性
function getString(something: string | number): string {
    return something.toString();
}

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

对象的类型——接口(interface)

使用接口(Interfaces)来定义对象的类型。

interface LabeledValue {
  label: string;
  width?: number; // 可选属性
  readonly y: number; // 只读属性
}
function printLabel(labeledObj: LabeledValue) {
  console.log(labeledObj.label);
}

const label1 = {
    label: 'drop',
    y: 20
}

const label2 = {
    label: 'throtle',
    y: 30,
    width: 30
}

如上所示,label1和label2都兼容接口LabeledValue,变量中的属性定义顺序跟接口中属性定义没关系。

数组类型

使用「类型 + 方括号」来表示数组

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

// 数组的项中不允许出现其他的类型:
let fibonacci: number[] = [1, '1', 2, 3, 5];

使用数组泛型(Array Generic) Array 来表示数组:

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

用接口描述数组,常用于类数组,eg:IArguments, NodeList, HTMLCollection

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

interface IArguments {
    [index: number]: any;
    length: number;
    callee: Function;
}

经常用any表示数组中可以出现任意对象

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

函数的类型

  1. 函数声明
  2. 函数表达式
  3. 接口定义
// 函数声明
function sum(x: number, y: number): number {
    return x + y;
}
// 函数表达式
let mySum: (x: number, y: number) => number = function (x: number, y: number): number {
    return x + y;
};
  • 在 TypeScript 的类型定义中,=> 用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型。
  • 用 ? 表示可选的参数,可选参数必须接在必需参数后面。
  • 允许默认参数值,当设置了默认值,会被认为是可选参数,此时不受上一条的影响。
  • 可以用数组类型定义剩余参数:...items: any[]

重载允许一个函数接受不同数量或类型的参数时,作出不同的处理。注意,TypeScript 会优先从最前面的函数定义开始匹配, 所以多个函数定义如果有包含关系,需要优先把精确的定义写在前面。

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

类型断言

类型断言(Type Assertion)可以用来手动指定一个值的类型。语法:值 as 类型(推荐) 或者 <类型>值

类型断言的用途

  1. 将一个联合类型断言为其中一个类型
interface Cat {
    name: string;
    run(): void;
}
interface Fish {
    name: string;
    swim(): void;
}
// 类型断言只能够「欺骗」TypeScript 编译器,无法避免运行时的错误,反而滥用类型断言可能会导致运行时错误
function isFish(animal: Cat | Fish) {
    if (typeof (animal as Fish).swim === 'function') {
        return true;
    }
    return false;
}
  1. 将一个父类断言为更加具体的子类
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;
}

// 也可以用instanceof来判断以上情况,但当ApiError 和 HttpError 不是一个真正的类,而只是一个 TypeScript 的接口(interface),就只能用断言来判断了
function isApiError(error: Error) {
    if (error instanceof ApiError) {
        return true;
    }
    return false;
}

  1. 将任何一个类型断言为 any
// 它极有可能掩盖了真正的类型错误,所以如果不是非常确定,就不要使用 as any。
(window as any).foo = 1;
  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();
  1. 要使得 A 能够被断言为 B,只需要 A 兼容 B 或 B 兼容 A 即可

需要注意的点

  1. 不是任何一个类型都可以被断言为任何另一个类型,要使得 A 能够被断言为 B,只需要 A 兼容 B 或 B 兼容 A 即可。
  2. 可以使用双重断言将任何一个类型断言为任何另一个类型,但十有八九是非常错误,除非迫不得已,千万别用双重断言。
  3. 类型断言只会影响 TypeScript 编译时的类型,不会真的影响到变量的类型。
  4. 类型声明是比类型断言更加严格。

声明文件

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

  • export as namespace UMD库声明全局变量
  • declare global扩展全局变量
  • declare module扩展模块
  • /// <refrence /> 三斜线指令

通常我们会把声明语句放到一个单独的文件(jQuery.d.ts)中,这就是声明文件,声明文件必需以 .d.ts 为后缀。 推荐使用 @types 统一管理第三方库的声明文件,使用方式: npm install @types/jquery --save-dev

可以在这里搜索第三方库的声明文件。当一个第三方库没有提供声明文件时,需要自己书写声明文件。 库的使用场景:

  1. 全局变量,通过script标签引进来
  2. npm包,符合es6规范
  3. UMD库,前两种方式都兼容
  4. 直接扩展全局变量,通过script标签引入之后,改变一个全局变量的结构
  5. 在npm包或UMD库中扩展全局变量,通过npm包引进来之后,改变一个全局变量的结构
  6. 模块插件:通过script或import导入后,改变另一个模块的结构

全局变量的声明

  • declare var 声明全局变量
declare var jQuery: (selector: string) => any;
declare let jQuery: (selector: string) => any;
declare const jQuery: (selector: string) => any;
// 切勿在声明语句中定义具体的实现
declare const jQuery = function(selector) {
    return document.querySelector(selector);
};
  • declare function 声明全局方法
declare function jQuery(selector: string): any;

// 支持重载
declare function jQuery(selector: string): any;
declare function jQuery(domReadyCallback: () => any): any;
  • declare class 声明全局类
declare class Animal {
    name: string;
    constructor(name: string);
    sayHi(): string;
}
// 不能用来定义具体的实现
declare class Animal {
    name: string;
    constructor(name: string);
    sayHi() {
        return `My name is ${this.name}`;
    };
    // ERROR: An implementation cannot be declared in ambient contexts.
}
  • declare enum 声明全局枚举类型
declare enum Directions {
    Up,
    Down,
    Left,
    Right
}
  • declare namespace 声明含子属性的全局对象
declare namespace jQuery {
    function ajax(url: string, settings?: any): void;
    const version: number;
    class Event {
        blur(eventType: EventType): void
    }
    enum EventType {
        CustomClick
    }
}
// 嵌套的命名空间
declare namespace jQuery {
    function ajax(url: string, settings?: any): void;
    namespace fn {
        function extend(object: any): void;
    }
}
  • interface 和 type 声明全局类型
interface AjaxSettings {
    method?: 'GET' | 'POST'
    data?: any;
}
declare namespace jQuery {
    function ajax(url: string, settings?: AjaxSettings): void;
}

// 将interface或者type放在namespace下,防止命名冲突
declare namespace jQuery {
    interface AjaxSettings {
        method?: 'GET' | 'POST'
        data?: any;
    }
    function ajax(url: string, settings?: AjaxSettings): void;
}
// 使用对应namespace下的interface需要带上对应的namespace
let settings: jQuery.AjaxSettings = {
    method: 'POST',
    data: {
        name: 'foo'
    }
};
// 声明合并:当一个全局变量既是一个函数,又是一个对象,可以组合多个声明语句
declare function jQuery(selector: string): any;
declare namespace jQuery {
    function ajax(url: string, settings?: any): void;
}

npm包的声明

npm包的声明一般可能存在两个地方:

  1. 查看npm包的package.json里的types字段,或者有个index.d.ts文件。(最推荐)
  2. 发布到@types里。尝试安装 npm i @types/foo --save-dev,如果能安装,就表示有声明文件。

如果以上两种方式都找不到对应的声明文件,则需要自己编写。有两种方式:

  1. 创建node_modules/@types/foo/index.d.ts文件,缺点:不易维护
  2. 创建types目录,管理自己的声明文件,需要在tsconfig.json中配置paths和baseUrl字段。目录如下:
├── src  
| └── index.ts
├── types  
| └── foo
|  └── index.d.ts 
└── tsconfig.json

tsconfig.json如下:

{
    "compilerOptions": {
        "module": "commonjs",
        "baseUrl": "./",
        "paths": {
            "*": ["types/*"]
        }
    }
}

module 配置可以有很多种选项,不同的选项会影响模块的导入导出模式。经常用commonjs这个选项。
npm包的声明文件的语法:

  1. export 导出变量
// export 的语法与普通的 ts 中的语法类似,区别仅在于声明文件中禁止定义具体的实现
export const name: string;
export function getName(): string;
export class Animal {
    constructor(name: string);
    sayHi(): string;
}
export enum Directions {
    Up,
    Down,
    Left,
    Right
}
export interface Options {
    data: any;
}

// 可以混用declare 和 export
declare const name: string;
declare function getName(): string;
declare class Animal {
    constructor(name: string);
    sayHi(): string;
}
declare enum Directions {
    Up,
    Down,
    Left,
    Right
}
interface Options {
    data: any;
}

export { name, getName, Animal, Directions, Options };
  1. export namespace 导出含子属性的对象
export namespace foo {
    const name: string;
    namespace bar {
        function baz(): string;
    }
}
  1. export defaut es6默认导出
export default function foo(): string;
// 只有 function、class 和 interface 可以直接默认导出,其他的变量需要先定义出来,再默认导出,针对这种默认导出,我们一般会将导出语句放在整个声明文件的最前面
export default Directions;
declare enum Directions {
    Up,
    Down,
    Left,
    Right
}
  1. export = commonjs导出模块
    • 在 commonjs 规范中,
// 整体导出
module.exports = foo;
// 单个导出
exports.bar = bar;
// 整体导入
const foo = require('foo');
// 单个导入
const bar = require('foo').bar;

// 还可以使用import
// 整体导入
import * as foo from 'foo';
// 单个导入
import { bar } from 'foo';


// 对于这种使用 commonjs 规范的库,假如要为它写类型声明文件的话,就需要使用到 export =
export = foo;

declare function foo(): string;
declare namespace foo {
    const bar: number;
}

内置对象

ECMAScript 常见的内置对象:

  1. Object
  2. Function
  3. Boolean
  4. Symbol
  5. Number
  6. BigInt
  7. Math
  8. Date
  9. String
  10. RegExp
  11. Array
  12. Map
  13. Set
  14. WeakMap
  15. WeakSet
  16. ……

DOM 和 BOM 的内置对象:

  1. Document
  2. HTMLElement
  3. Event
  4. NodeList

它们可以直接在 TypeScript 中当做定义好了的类型。

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

npm install @types/node --save-dev

类型注解

轻量级,为函数或变量添加约束的方式。

额外的属性检查

[[对象字面量]]会被特殊对待而且会经过额外属性检查。 当将它们赋值给变量或作为参数传递的时候, 如果一个对象字面量存在任何“目标类型”不包含的属性时,你会得到一个错误。

interface Music {
    x?:string,
    y?:string,
    z?:string
}
function sing(music: Music):viod {
    console.log(`${music.x}`)
}

sing({ a: '123', x: '345'}) // 报错 类型“{a:string;}”与类型Music不具备有相同的属性

解决方法1:改变interface

interface Music {
    x?:string,
    y?:string,
    z?:string,
    [props: string]: any
}

解决方法2:使用as

sing({ a: '123', x: '345'} as Music)

解决方法3:使用变量(不推荐)

let music1 = { a: '123', x: '345'}
sing( music1 as Music)

函数类型

interface Phone {
    (color:string, width:number, height:number): void
}
let phone : Phone ;
phone = function(c: string, w: number, h:number):void {
    console.log(c)
    console.log(w)
    console.log(h)
}
phone('red', 12, 8)

可索引的类型

可索引类型具有一个 索引签名,它描述了对象索引的类型,还有相应的索引返回值类型。
支持两种索引签名:字符串和数字
可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型

interface Names {
    [index:number]: string; // 索引类型为number,索引返回类型为string,表示用number类型的值去访问Names会返回string类型的值
}

interface Indexs {
    [index: string]: string;
    [index: number]: string;
    name: string;
    color: string;
    num: number; //错误,与索引返回类型不匹配
}

let names:Names = ['li', 'zhang', 'deng']
console.log(names[0])

类类型

明确的强制一个类去符合某种契约
接口描述了类的公共部分,不会检查私有成员

interface Clock {
    currentTime: string // 索引类型为number,索引返回类型为string,表示用number类型的值去访问Names会返回string类型的值
}

class DayClock implements Clock{
    currentTime: string;
    constructor() {
        
    }
}

包含公共字段和构造函数

class Greeter {
    greeting: string;
    constructor(msg: string) {
        this.greeting = msg;
    }
    greet() {
        return "hello, "+ this.greeting;
    }
}
let greeter = new Greeter('world')

继承:extents public: 公共,默认
private: 私有, protected: 受保护的

readonly和const的区别

readonly:定义接口或类的属性是只读的
const:定义不可变的变量

更新于: 2022年5月5日