Typescript学习文档 - zptime/blog GitHub Wiki

学习文档:主要参考第三个,ts入门到精通

前期准备

TypeScript 是由微软公司在 2012 年正式发布,是 JavaScript 的超集,可以编译成纯 JavaScript。

TypeScript 增加了类、模块、接口和类型注解等等,提供了静态检查和类型推断,面向对象编程。

编辑器主要是VSCode

1. 开发环境搭建

// node安装 v14.17.3
node -v 

// npm安装 v8.1.3
npm -v

// typescript全局安装
npm install typescript -g
yarn global add typescript

2. mac命令介绍

// 创建文件夹
mkdir demo

// 删除文件夹(不会出现在废纸篓,空目录)
rmdir demo

// 删除文件夹(不会出现在废纸篓,非空或者空都可以删除,推荐使用)
rm -rf demo

// 移动或重命名一个目录
mvdir demo demoTS

// 创建文件
touch Demo1.ts

// 更改文件名字,暂时没找到,手动改成demo1.ts了

// 删除文件
rm demo1.ts

// 拷贝文件demo1.ts,并且重命名为demo2.ts
cp demo1.ts demo2.ts

// 查找当前目录下所有的ts文件
find *.ts

// 显示当前目录 /Users/xxx/Documents/projects/demo
pwd

// 打开成可视化的文件夹
open . // 打开当前命令里的目录
open demo1.ts // 打开指定的文件夹或者文件

// 显示当前目录的内容
ls -la // 查看全部详细信息
ls // 仅查看文件

// 显示或者链接文件
cat demo1.ts

// 查看并编辑文件
vim demo.ts

// 比较两个目录的内容
dircmp dir1 dir2

3. 编译ts文件

mkdir demo // 创建目录
touch demo1.ts // 创建文件
open demo1.ts // 打开文件并可以进行编辑

node demo1.ts // 运行文件,会报错(SyntaxError: Unexpected token ':'),原因就是 Node 不能直接运行TypeScript文件

tsc demo1.ts // 将ts转换为js文件,产生 demo1.js 文件
node demo1.js // 运行js文件,打印结果 “Hello World”

npm install -g ts-node // 全局安装插件,可自动完成ts文件的编译和运行,避免每次手动转化
ts-node -v // 查看版本号 v10.4.0
ts-node demo1.ts // 直接运行ts文件,正常输出“Hello World”

ic_mac_command

TypeScript基础知识

1. 静态类型

意义:变量的类型不可以改变,类型的属性和方法也确定了

优点:提高了程序的健壮性,vscode也会有语法提示,加快开发效率

分类:

  • 基础静态类型:nunber, string, null, undefinde, symbol, boolean, void
  • 对象类型:对象object, 数组array, 类new Person(),函数()=>{}
// 基础定义
const count: number = 1;

// 自定义 通过接口
interface XiaoJieJie {
  uname: string;
  age: number;
}
const xiaohong: XiaoJieJie = {
  uname: "小红",
  age: 18,
};

// 类类型
class Person {}
const dajiao: Person = new Person();

// 函数类型
const jianXiaoJieJie: () => string = () => {
  return "大脚";
};

2. 类型注解和类型推断

基础概念

  • 类型注解:我们告诉代码变量的类型

  • 类型推断:在有些没有明确指出类型的地方,代码自动分析变量的类型

工作使用问题(潜规则)

  • 如果 TS 能够自动分析变量类型, 就什么也不需要做了
  • 如果 TS 无法分析变量类型的话, 就需要使用类型注解

3. 函数参数和返回类型定义

// 简单类型定义
function getTotal(one: number, two: number): number {
  return one + two;
}
const total = getTotal(1, 2);

// 函数无返回值时定义 void
function sayHello(): void {
  console.log("hello world");
}

// never返回值类型:函数用选执行不完,如抛出异常或者死循环
function errorFuntion(): never {
  throw new Error();
  console.log("Hello World");
}

function forNever(): never {
  while (true) {}
  console.log("Hello JSPang");
}

// 函数参数为对象(结构)时
function add({ one, two }: { one: number, two: number }): number {
  return one + two;
}
const three = add({ one: 1, two: 2 });

4.数组类型定义

// 一般数组
const numberArr: number[] = [1, 2, 3];
const stringArr: string[] = ["a", "b", "c"];
const arr: (number | string)[] = [1, "string", 2];

// 对象数组
// 1.使用类型别名定义(type关键字)
type Lady = { name: string, age: Number }; // Lady 的别名
const xiaoJieJies: Lady[] = [
  { name: "刘英", age: 18 },
  { name: "谢大脚", age: 28 },
];
// 2.使用类进行定义(class关键字)
class Madam {
  name: string;
  age: number;
}
const xiaoJieJies: Madam[] = [
  { name: "刘英", age: 18 },
  { name: "谢大脚", age: 28 },
];

5.元组使用和类型约束

// 数组:没法完全限制
const xiaojiejie: (string | number)[] = ["dajiao", "teacher", 28];
const xiaojiejie: (string | number)[] = ["dajiao", 28, "teacher"];

// 元组:数组的加强,把数组中的每个元素类型的位置固定住
const xiaojiejie: [string, string, number] = ["dajiao", "teacher", 28];
const xiaojiejies: [string, string, number][] = [
  ["dajiao", "teacher", 28],
  ["liuying", "teacher", 18],
  ["cuihua", "teacher", 25],
];

6.接口 interface

解决代码重复的问题:类型别名(type),类(class),接口(interface)

接口(interface):接口只是对开发的约束,在生产环境中并没有体现。接口只是在 TypeScript 里作语法校验的工具,编译成正式的js代码,就不会有任何用处了。

  • 类型别名:可以代表普通类型,如 string
  • 接口:必须代表对象;可以定义非必选值(?);允许加入任意值;可以定义方法;接口和类结合;接口继承
// 类型别名定义
type Girl = stirng;

// 接口定义,规范类型
interface Girl {
  name: string;
  age: number;
  gender?: number; // 非必选值
  [propname: string]: any; // 任意值,属性的名字是字符串类型,属性的值可以是任何类型
  say(): string; // say()方法,返回值是string类型
}

const getResume = (girl: Girl) => {
  console.log(girl.name + "姓名是:" + girl.name);
  console.log(girl.age + "年龄是:" + girl.age);
};

// 接口和类的约束
class XiaoJieJie implements Girl {
  name = "谢谢";
  age = 18;
  say() {
    return "欢迎光临!";
  }
}

// 接口间的继承
interface Teacher extends Girl {
  teach(): string;
}

7.类 class

基础介绍

// 1.基本使用:可以定义函数和方法class Lady {  content = "Hi,帅哥";  sayHello() {    return this.content;  }}// 2.类的继承(extends)class XiaoJieJie extends Lady {  sayLove() {    return "I love you";  }  // 3.类的重写:此处重写了 sayHello 方法  sayHello() {    // return "Hi , honey!";    // 4.super关键字的使用:代表父类对象    return super.sayHello() + "。你好!";  }}const goddess = new Lady();console.log(goddess.sayHello());console.log(goddess.sayLove());

访问类型

  • public:公共的,允许在类的内部和外部被调用
  • protedred:受保护的,允许在类内及继承的子类中使用
  • private:私有的,只允许在类的内部被调用,外部不允许调用
class Person {  public name:string;  private age: number;  protected sex:string;  public sayHello(){    console.log(this.name + ' say Hello')  }}const person = new Person();person.name = "jspang.com";person.age = 24console.log(person.name); // 正常console.log(person.age); // 报错

构造函数

构造函数:在类被初始化的时候,自动执行的一个方法

// 基础写法class Person{  public name:string ;  constructor(name:string){    this.name=name  }}const person= new Person('jspang')console.log(person.name)// 继承中的写法(super)class Person{  constructor(public name:string){}}class Teacher extends Person{  // 子类继承父类并有构造函数的原则:在子类里写构造函数时,必须用super()调用父类的构造函数,如果需要传值,也必须进行传值操作  constructor(public age:number){      super('jspang')  }}const teacher = new Teacher(18)console.log(teacher.age)console.log(teacher.name)

Getter、Setter和static使用

类的访问类型private,最大用处是封装一个属性,然后通过 Getter 和 Setter 的形式来访问和修改这个属性。

类中的static:正常情况下想使用类的实例,就要先New出来;而用static声明的属性和方法,不需要进行声明对象,就可以直接使用

// Getter 和 Setter 的使用class Xiaojiejie {  constructor(private _age:number){}  get age(){    return this._age - 20  }  set age(age:number){    this._age = age + 3  }}const dajiao = new Xiaojiejie(28)dajiao.age=25console.log(dajiao.getAge)// 静态修饰符 static 的使用class Girl {  static sayLove() {    return "I Love you";  }}console.log(Girl.sayLove());

只读属性和抽象类

抽象类:跟父类很像,都需要继承,但是抽象类里一般都有抽象方法。继承抽象类的类必须实现抽象方法才可以。

class Person {  public readonly _name :string; // readonly,只读属性,不能修改  constructor(name:string ){    this._name = name; // 会报错  }}// 抽象类abstract class Girl{  abstract skill()  //抽象方法:因为没有具体的方法,所以我们这里不写括号}class Waiter extends Girl{  skill(){    console.log('服务员,倒水!')  }}class BaseTeacher extends Girl{  skill(){    console.log('初级技师,泰式按摩!')  }}

8.配置文件tsconfig.json

编译配置详细文档:https://www.tslang.cn/docs/handbook/compiler-options.html

tsc --init // 生成tsconfig.json编译配置文件tsc demo1.ts // ts文件编译为js文件,但tsconfig.json未生效tsc // 直接运行该命令,tsconfig.json生效了
ic_tsc_1
{ /*注意:配置文件不支持单引号,里面必须使用双引号*/ // "include": ["demo1.ts"], // 指定要编译的文件,包含文件 // "exclude": ["demo2.ts"], // 不包含的文件 // "files":["demo1.ts"], // 配置效果和include几乎一样 "compilerOptions": {    /* Visit https://aka.ms/tsconfig.json to read more about this file */    "removeComments": true, /* 去除注释 */    "strict": true, /* 严格模式 编译和书写规范 */    "noImplicitAny": false, /* 允许你的注解类型any不用特意表明。为true代表any值也需要类型注释,不然报错 */    "strictNullChecks": false, /* 不强制检查 NULL 类型 */    // "outDir": "./build", // 编译后js文件存放目录    "rootDir": "./src", // ts文件位置目录    "target":"es5" ,  // 默认是开启的,必须开启才能转换成功(es6语法转为es5语法),和allowJs连用    "allowJs":true,   // 联通,两项都开启后,在使用tsc编译时,就会编译js文件了    "sourceMap": true, /* 位置信息文件 */    "noUnusedLocals": true, /* 无用代码提示,避免无用代码编译,减少资源浪费 */    "noUnusedParameters": true, /* 无用函数提示 */  }}
ic_tsc_2

9.联合类型和类型保护

注意:只有联合类型存在的情况下,才需要类型保护

联合类型(|):可以认为一个变量可能有两种或两种以上的类型

类型保护:不能判断联合类型具体的实例是什么,会报错,就需要类型保护处理

  • 类型断言:通过断言的方式确定传递过来的准确值(as)
  • in语法(if/else + in)
  • typeof语法
  • instanceof语法:类型保护的是一个对象,只能用在类上
interface Waiter {  anjiao: boolean;  say: () => {};}interface Teacher {  anjiao: boolean;  skill: () => {};}// 联合类型function judgeWho(animal: Waiter | Teacher) {  // 1. 类型断言  if (animal.anjiao) {    (animal as Teacher).skill();  }else{    (animal as Waiter).say();  }    // 2. in语法  if ("skill" in animal) {    animal.skill();  } else {    animal.say();  }}// 3.typeof语法function add(first: string | number, second: string | number) {  if (typeof first === "string" || typeof second === "string") {    return `${first}${second}`;  }  return first + second;}// 4. instanceof语法class NumberObj {  count: number;}function addObj(first: object | NumberObj, second: object | NumberObj) {  if (first instanceof NumberObj && second instanceof NumberObj) {    return first.count + second.count;  }  return 0;}

10.Enum枚举类型

// 枚举类型的对应值:默认从0开始enum Status {  MASSAGE, // 0  SPA, // 1  DABAOJIAN, // 2}// 也可自定义初始值,之后的按顺序累加enum Status {  MASSAGE = 1, // 1  SPA, // 2  DABAOJIAN, // 3}// 枚举通过下标反查:可以打印出枚举的值,也可通过下标反查得到枚举的值console.log(Status.MASSAGE, Status[1]);

11.泛型(难点)

泛型:泛指的类型,使用<>(尖角号)进行定义

// 1. 函数中的泛型// 变量的使用function join<T>(first: T, second: T) {  return `${first}${second}`;}join < string > ("hello", "world");join < number > (1, 2);// 数组的使用:直接使用[];使用Array<泛型>function myFun<T>(params: T[] 或者 Array<T>) {  return params;}myFun < string > ["123", "456"];// 多个泛型:如果函数定义了多个泛型,使用时要对应的定义出具体的类型function join<T, P>(first: T, second: P) {  return `${first}${second}`;}join < number, string > (1, "2");join(1, "2"); // 支持类型推断,不会报错// 2. 类中的泛型// 基本类class SelectGirl {  constructor(private girls: string[] | number[]) {}  getGirl(index: number): string | number {    return this.girls[index];  }}// 初始类的泛型class SelectGirl<T> {  constructor(private girls: T[]) {}  getGirl(index: number): T {    return this.girls[index];  }}const selectGirl = new SelectGirl() < string > ["大脚", "刘英", "晓红"];console.log(selectGirl.getGirl(1));// 泛型中的继承(extends)interface Girl {  name: string;}class SelectGirl<T extends Girl> {  constructor(private girls: T[]) {}  // getGirl方法的返回类型,应该是一个string类型  getGirl(index: number): string {    // 传递过来的值必须是一个对象类型,必须有name属性    return this.girls[index].name;  }}const selectGirl = new SelectGirl([  { name: "大脚" },  { name: "刘英" },  { name: "晓红" },]);console.log(selectGirl.getGirl(1));// 泛型约束// 之前可以是任意类型,改造后必须是string或者number类型class SelectGirl<T extends number | string> {  //.....}

12. 搭建浏览器开发环境步骤

上一节可查看“8.配置文件tsconfig.json”

如何搭建一个最基础的 TS 开发环境:

// 1. 创建文件夹,创建package.json文件mkdir tsdemocd tsdemonpm init -y // add package.json// 2. 生成tsconfig.json文件tsc -init// 3. 新建src和build文件夹,新建index.html文件mkdir srcmkdir buildtouch index.html// 4. src下新建page.ts文件cd srctouch page.ts// 5. 配置tsconfig.json文件,设置outDir和rootDiropen tsconfig.json"rootDir": "./src" /* ts文件位置目录 */"outDir": "./build", /* 编译后js文件存放目录 */// 6. 编写index.htmlopen index.html // 打开文件,添加如下内容
<!DOCTYPE html><html lang="en">  <head>    <meta charset="UTF-8" />    <meta name="viewport" content="width=device-width, initial-scale=1.0" />    <script src="./build/page.js"></script>    <title>Document</title>  </head>  <body></body></html>
// 7. 编写page.ts文件, console.log('hello world!')open page.ts // 打开文件,添加输出 tsc // 转译文件,生成build/page.js文件// 8. 浏览器中查看index.html文件,控制台会输出 “hello world!”
ic_tsc_3

TS在浏览器运行方式对比

ts-node可以直接运行ts文件,但是浏览器是不能直接运行ts的,需要转成js。js有不同的规范,所以ts转js时,可以转换的目标规范也可以不同,具体配置如图所示:

ic_tsc_10

以下三种方式:

  • 命名空间使用:CommonJS规范
  • import方式引入:ES6 Module,转译成AMD规范
  • Parcel打包使用:打包工具方式

13.命名空间Namespace

没有命名空间的问题

  • 全部都是var声明的变量,都是全局变量,控制台可以直接输出
  • 过多的全局变量会让代码变的不可维护

命令空间的使用

  • 通过 namespace 定义,如namespace Home
  • 通过 export 暴露出去,只有暴漏出去的类是全局的,其他的不会造成全局污染
  • tsc -w // 监视,只要有更新就会重新编译
  • 好处就是让全局变量减少了很多,实现了基本的封装,减少了全局变量的污染。

命名空间实现组件化

  • 主页:通过Home.Page可访问
  • 组件化:namespace Components,通过Components.Header可访问
  • 子命名空间:通过Components.SubComponents.Test可访问;
// src/components.tsnamespace Components {  // 子命名空间  export namespace SubComponents {    export class Test {}  }    export class Header {    constructor() {      const elem = document.createElement("div");      elem.innerText = "This is Header";      document.body.appendChild(elem);    }  }  export class Content {    constructor() {      const elem = document.createElement("div");      elem.innerText = "This is Content";      document.body.appendChild(elem);    }  }  export class Footer {    constructor() {      const elem = document.createElement("div");      elem.innerText = "This is Footer";      document.body.appendChild(elem);    }  }}// src/page.tsnamespace Home {  export class Page {    constructor() {      new Components.Header();      new Components.Content();      new Components.Footer();    }  }}// tsconfig.json文件:多文件编译成1个文件// 配置前会有page.js和components.js两个文件,配置后只有page.js一个文件{  "outFile": "./build/page.js",  // "module": "commonjs",  "module": "amd",}
ic_tsc_4 ic_tsc_6

14. import的使用

  • CommonJS:浏览器不支持,用于服务端,如NodeJS,webpack;通过require同步加载模块,通过 exports 或 module.exports 导出
  • AMD:异步模块定义,支持浏览器端,RequireJS;通过define定义模块,require加载模块;提前执行,依赖前置
  • CMD:同步模块定义,支持浏览器端,SeaJS;延迟执行,依赖就近;
  • ES6 Module:import/export;无法直接在浏览器中执行,需要转译
// components.ts ES6的export导出export class Header {  constructor() {    const elem = document.createElement("div");    elem.innerText = "This is Header";    document.body.appendChild(elem);  }}export class Content {  constructor() {    const elem = document.createElement("div");    elem.innerText = "This is Content";    document.body.appendChild(elem);  }}export class Footer {  constructor() {    const elem = document.createElement("div");    elem.innerText = "This is Footer";    document.body.appendChild(elem);  }}// page.ts文件 ES6的 import 引入import { Header, Content, Footer } from "./components";export default class Page {  constructor() {    new Header();    new Content();    new Footer();  }}

tsc编译后,代码都是define开头的(这是 amd 规范的代码,不能直接在浏览器中运行可以在 Node 中直接运行),这种代码在浏览器中是没办法被直接运行的,需要其他库(require.js)的支持。

ic_tsc_5

引入require,如下所示:修改后,可以正常在浏览器中显示

<!DOCTYPE html><html lang="en">  <head>    <meta charset="UTF-8" />    <meta name="viewport" content="width=device-width, initial-scale=1.0" />    // 引入文件    <script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.js"></script>    <script src="./build/page.js"></script>    <title>Document</title>  </head>  <body>    // 更改使用方式    <script>      require(["page"], function (page) {        new page.default();      });  	</script>  </body></html>

15. 用Parcel打包TS代码

上面require方式引入,用起来比较麻烦。

可以通过webpack和Parcel打包工具处理,简化流程。

Parcel官网地址:https://v2.parceljs.cn/getting-started/webapp/

还是借助上面 tsdemo 示例进行改造:

  1. 更改index.html文件,直接引入ts文件;且移动到src目录下
<!DOCTYPE html><html lang="en">  <head>    <meta charset="UTF-8" />    <meta name="viewport" content="width=device-width, initial-scale=1.0" />    // 必须要加type="module",不然会报错    <script type="module" src="./page.ts"></script>    <!-- <script src="./build/page.js"></script> -->    <title>Document</title>  </head>  <body>    // page.ts最后面加上 "new Page()" 执行代码,不在index.html中    <!-- <script>new Page(); </script> -->  </body></html>
  1. 修改tsconfig.json配置文件:只更改 rootDir 的值,其他的还原
{   "compilerOptions": {     "target": "es5",    "module": "commonjs"    // "module": "amd",    // "outFile": "./build/page.js",    "rootDir": "./src",    // "outDir": "./build",    }}
  1. 安装Parcel
// yarn 安装yarn add --dev parcel// 或者 npm 安装npm install --save-dev parcel
  1. 修改package.json文件
{
  "name": "tsdemo",
  "version": "1.0.0",
  "source": "src/index.html", // add
  "scripts": {
    "start": "parcel", // add
    "build": "parcel build" // add
  },
  "devDependencies": {
    "parcel": "^2.0.1" // add
  }
}
  1. 启动服务
// 运行命令
npm run start
// 打开页面访问 
http://localhost:1234

报错如下图所示:@parcel/transformer-js: Browser scripts cannot have imports or exports.

ic_tsc_8

报错原因:代码中用到了ES6的import/export,Parcel不支持

解决办法:就是引入时添加 type="module"

  1. 效果展示
ic_tsc_9
⚠️ **GitHub.com Fallback** ⚠️