Typescript学习文档 - zptime/blog GitHub Wiki
学习文档:主要参考第三个,ts入门到精通
TypeScript 是由微软公司在 2012 年正式发布,是 JavaScript 的超集,可以编译成纯 JavaScript。
TypeScript 增加了类、模块、接口和类型注解等等,提供了静态检查和类型推断,面向对象编程。
编辑器主要是VSCode
// node安装 v14.17.3
node -v
// npm安装 v8.1.3
npm -v
// typescript全局安装
npm install typescript -g
yarn global add typescript
// 创建文件夹
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
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”
意义:变量的类型不可以改变,类型的属性和方法也确定了
优点:提高了程序的健壮性,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 "大脚";
};
基础概念
-
类型注解:我们告诉代码变量的类型
-
类型推断:在有些没有明确指出类型的地方,代码自动分析变量的类型
工作使用问题(潜规则)
- 如果
TS
能够自动分析变量类型, 就什么也不需要做了 - 如果
TS
无法分析变量类型的话, 就需要使用类型注解
// 简单类型定义
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 });
// 一般数组
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 },
];
// 数组:没法完全限制
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],
];
解决代码重复的问题:类型别名(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;
}
// 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)
类的访问类型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('初级技师,泰式按摩!') }}
编译配置详细文档:https://www.tslang.cn/docs/handbook/compiler-options.html
tsc --init // 生成tsconfig.json编译配置文件tsc demo1.ts // ts文件编译为js文件,但tsconfig.json未生效tsc // 直接运行该命令,tsconfig.json生效了
{ /*注意:配置文件不支持单引号,里面必须使用双引号*/ // "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, /* 无用函数提示 */ }}
注意:只有联合类型存在的情况下,才需要类型保护
联合类型(|):可以认为一个变量可能有两种或两种以上的类型
类型保护:不能判断联合类型具体的实例是什么,会报错,就需要类型保护处理
- 类型断言:通过断言的方式确定传递过来的准确值(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;}
// 枚举类型的对应值:默认从0开始enum Status { MASSAGE, // 0 SPA, // 1 DABAOJIAN, // 2}// 也可自定义初始值,之后的按顺序累加enum Status { MASSAGE = 1, // 1 SPA, // 2 DABAOJIAN, // 3}// 枚举通过下标反查:可以打印出枚举的值,也可通过下标反查得到枚举的值console.log(Status.MASSAGE, Status[1]);
泛型:泛指的类型,使用<>
(尖角号)进行定义
// 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> { //.....}
上一节可查看“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!”
ts-node可以直接运行ts文件,但是浏览器是不能直接运行ts的,需要转成js。js有不同的规范,所以ts转js时,可以转换的目标规范也可以不同,具体配置如图所示:
以下三种方式:
- 命名空间使用:CommonJS规范
- import方式引入:ES6 Module,转译成AMD规范
- Parcel打包使用:打包工具方式
没有命名空间的问题
- 全部都是
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",}
- 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
)的支持。
引入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>
上面require方式引入,用起来比较麻烦。
可以通过webpack和Parcel打包工具处理,简化流程。
Parcel官网地址:https://v2.parceljs.cn/getting-started/webapp/
还是借助上面 tsdemo 示例进行改造:
- 更改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>
- 修改tsconfig.json配置文件:只更改 rootDir 的值,其他的还原
{ "compilerOptions": { "target": "es5", "module": "commonjs" // "module": "amd", // "outFile": "./build/page.js", "rootDir": "./src", // "outDir": "./build", }}
- 安装Parcel
// yarn 安装yarn add --dev parcel// 或者 npm 安装npm install --save-dev parcel
- 修改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
}
}
- 启动服务
// 运行命令
npm run start
// 打开页面访问
http://localhost:1234
报错如下图所示:@parcel/transformer-js: Browser scripts cannot have imports or exports.
报错原因:代码中用到了ES6的import/export,Parcel不支持
解决办法:就是引入时添加 type="module"
- 效果展示