TypeScript
环境TypeScript
代码的编译与运行https://www.typescriptlang.org/zh/
TypeScript
编写的程序并不能直接通过浏览器运行,需要先通过 TypeScript
编译器把TypeScript
代码编译成 JavaScript
代码TypeScript
的编译器是基于Node.js
的,所以需要先安装Node.js
终端
或者 cmd
等命令行工具来调用 node
# 查看当前 node 版本
node -v
通过 NPM
包管理工具安装 TypeScript
编译器
npm i -g typescript
安装完成以后,我们可以通过命令 tsc
来调用编译器
# 查看当前 tsc 编译器版本
tsc -v
vsCode
和 TypeScript
都是微软的产品, vsCode
本身就是基于 TypeScript
进行开发的, vsCode
对 TypeScript
有着天然友好的支持TypeScript
的文件的后缀为 .ts
// ./src/helloKaiKeBa.ts
let str: string = '开课吧';
TypeScript
编译器 tsc
对 .ts
文件进行编译#此时文件是在src的上一级
tsc ./src/helloKaiKeBa.ts
指定编译文件输出目录
tsc --outDir ./dist ./src/helloKaiKeBa.ts
指定编译的代码版本目标,默认为 ES3
ES3
中,let
会被转成var
ES6
中,let
会被转成let
ES3
中,对象最后一个属性后面的逗号是不允许存在的,ES5以后就可以tsc --outDir ./dist --target ES6 ./src/helloKaiKeBa.ts
在监听模式下运行,当文件发生改变的时候自动编译
tsc --outDir ./dist --target ES6 --watch ./src/helloKaiKeBa.ts
TypeScript
编译为提供了一个更加强大且方便的方式,编译配置文件tsConfig.json
json
文件中,默认情况下 tsc
命令运行的时候会自动去加载运行命令所在的目录下的 tsconfig.json
文件,配置文件格式如下{
"compilerOptions": {
"outDir": "./dist",
"target": "es5",
"watch": true,
// 检测null和undefined的标注
"strictNullChecks": true,
// 函数参数出现隐含的any时会报错
"noImplicitAny": true,
"lib": ["ES6", "DOM"],
// 导入导出用commonjs还是ESM,见9.6 模块编译
"module": "es6",
// 允许加载JS文件
"allowJs": true,
// 导入导出策略
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
//允许加载json
"resolveJsonModule": true,
// 解析策略
"moduleResolution": "node",
// 装饰器是否可用
"experimentalDecorators": true,
// 开启后会给类、方法、访问符、属性、参数等添加几个元数据
"emitDecoratorMetadata": true
},
// ** : 所有目录(包括子目录)
// * : 所有文件,也可以指定类型 *.ts
"include": ["./src/**/*"]
}
tsc
TypeScript
在编译过程中只会转换语法,比如扩展运算符,箭头函数等API
是不会进行转换的(也没必要转换,而是引入一些扩展库进行处理的)target
中没有的 API
,则需要手动进行引入TypeScript
会根据target
载入核心的类型库
target
为 es5
时: ["dom", "es5", "scripthost"]
target
为 es6
时: ["dom", "es6", "dom.iterable", "scripthost"]
--project
或 -p
指定配置文件目录,会默认加载该目录下的 tsconfig.json
文件tsconfig.json
这个文件是存放在configs
文件夹下的# configs文件夹下只有一个文件
tsc -p ./configs
也可以指定某个具体的配置文件
tsc -p ./configs/ts.json
可以直接运行ts文件,不需要经过编译
npm i -g ts-node
ts-node ./src/1-装饰器.ts
程序 = 数据结构 + 算法 = 各种格式的数据 + 处理数据的逻辑
类型系统包含两个重要组成部分
TypeScript
编译器就能按照标注对这些数据进行类型合法检测TypeScript
中,类型标注的基本语法格式为
TypeScript
的类型标注,我们可以分为
基础类型包含:string
,number
,boolean
标注语法
let title: string = '开课吧';
let n: number = 100;
let isOk: boolean = true;
因为在 Null
和 Undefined
这两种类型有且只有一个值,在标注一个变量为 Null
和 Undefined
类型,那就表示该变量不能修改了
let a: null;
// ok
a = null;
// error
a = 1;
默认情况下 null
和 undefined
是所有类型的子类型。 就是说可以把 null
和 undefined
赋值给其它类型的变量
也就是说声明了其他类型的时候可以赋值为null或者
undefined`
let a: number;
// ok
a = null;
如果一个变量声明了,但是未赋值,那么该变量的值为 undefined
,但是如果它同时也没有标注类型的话,默认类型为 any
, any
类型后面有详细说明
// 类型为 `number`,值为 `undefined`
let a: number;
// 类型为 `any`,值为 `undefined`
因为 null
和 undefined
都是其它类型的子类型,所以默认情况下会有一些隐藏的问题
let a:number;
a = null;
// ok(实际运行是有问题的)
a.toFixed(1);
小技巧:指定 strictNullChecks
配置为 true
,可以有效的检测 null
或者 undefined
,避免很多常见问题,也可以使程序编写更加严谨
tsconfig.json
中添加配置null
类型赋值给其他类型let a:number;
a = null;
// error
a.toFixed(1);
let ele = document.querySelector('div');
// 获取元素的方法返回的类型可能会包含 null,所以最好是先进行必要的判断,再进行操作
if (ele) {
ele.style.display = 'none';
}
在 JavaScript
中,有许多的内置对象,比如:Object
、Array
、Date
、RegExp
……,我们可以通过对象的 构造函数 或者 类 来进行标注
注意,这个字母开头是大写
let a: object = {};
// 数组这里标注格式有点不太一样,后面我们在数组标注中进行详细讲解
let arr: Array<number> = [1,2,3];
let d1: Date = new Date();
另外一种情况,许多时候,我们可能需要自定义结构的对象。这个时候,我们可以:
let a: {username: string; age: number} = {
username: 'zMouse',
age: 35
};
// ok
a.username;
a.age;
// error
a.gender;
// 这里使用了 interface 关键字,在后面的接口章节中会详细讲解
// 里面用的分号
interface Person {
username: string;
age: number;
};
let a: Person = {
username: 'zMouse',
age: 35
};
// ok
a.username;
a.age;
// error
a.gender;
class
,只是定义个解构用interface
// 类的具体使用,也会在后面的章节中讲解
class Person {
constructor(public username: string, public age: number) {
}
}
// ok
a.username;
a.age;
// error
a.gender;
// 函数
interface AjaxOptions {
url: string;
method: string;
}
function ajax(options: AjaxOptions) {}
ajax({
url: '',
method: 'get'
});
JavaScript
中的 String
、 Number
、 Boolean
string
类型 和 String
类型并不一样,在 TypeScript
中也是一样
let a: string;
a = '1';
// error String有的,string不一定有(对象有的,基础类型不一定有)
a = new String('1');
let b: String;
b = new String('2');
// ok 和上面正好相反
b = '2';
TypeScript
中的数组是:一类具有相同特性的数据的有序结合
TypeScript
中数组存储的类型必须一致,所以在标注数组类型的时候,同时要标注数组中存储的数据类型
// 表示数组中存储的数据类型,泛型具体概念后续会讲
let arr1: Array<number> = [];
// ok
arr1.push(100);
// error
arr1.push('开课吧');
let arr2: string[] = [];
// ok
arr2.push('开课吧');
// error
arr2.push(1);
元组类似数组,但是存储的元素类型不必相同,但是需要注意:
let data1: [string, number] = ['开课吧', 100];
// ok
data1.push(100);
// ok
data1.push('100');
// error
data1.push(true);
枚举的作用组织收集一组关联数据的方式,通过枚举我们可以给一组有关联意义的数据赋予一些友好的名字
注意事项:
key
不能是数字value
enum HTTP_CODE {
OK = 200,
NOT_FOUND = 404,
METHOD_NOT_ALLOWED
};
// 200
HTTP_CODE.OK;
// 405
HTTP_CODE.METHOD_NOT_ALLOWED;
// error
HTTP_CODE.OK = 1;
// 上述编译后的结果
// HTTP_CODE['OK']=200
// HTTP_CODE[200]='OK'
// 自枚举
var HTTP_CODE;
(function (HTTP_CODE) {
HTTP_CODE[HTTP_CODE["OK"] = 200] = "OK";
HTTP_CODE[HTTP_CODE["NOT_FOUND"] = 404] = "NOT_FOUND";
HTTP_CODE[HTTP_CODE["METHOD_NOT_ALLOWED"] = 405] = "METHOD_NOT_ALLOWED";
})(HTTP_CODE || (HTTP_CODE = {}));
enum URLS {
USER_REGISETER = '/user/register',
USER_LOGIN = '/user/login',
// 如果前一个枚举值类型为字符串,则后续枚举项必须手动赋值
INDEX = 0
}
void
strictNullChecks
为 false
的情况下, undefined
和 null
都可以赋值给 void
strictNullChecks
为 true
的情况下,只有 undefined
才可以赋值给 void
function fn():void {
// 没有 return 或者 return undefined
}
return
的时候,返回的就是 never
void
不同, void
是执行了return
, 只是没有值never
是不会执行 return
,比如抛出错误,导致函数终止执行function fn(): never {
throw new Error('error');
}
any
类型any
类型也可以赋值给任意类型any
类型有任意属性和方法any
类型,也意味着放弃对该值的类型检测,同时放弃 IDE 的智能提示noImplicitAny
配置为 true
,当函数参数出现隐含的 any
类型时报错// 隐式的any
let a;
let a1:any;
a1='a'
a1='2'
let b:number;
b=a;
unknow
,3.0 版本中新增,属于安全版的 any
,但是与 any
不同的是:
unknow
仅能赋值给 unknow
、any
unknow
没有任何属性和方法let c:unknown='开课吧';
let b:number=1;
b.toFixed(1);
// 下面会报错
b=c;
在 JavaScript
函数是非常重要的,在 TypeScript
也是如此。同样的,函数也有自己的类型标注格式
function add(x: number, y: number): number {
return x + y;
}
TypeScript
的核心之一就是对值(数据)所具有的结构就行类型检查// 定义接口
// 多个属性之间可以用逗号或者分号进行分隔
interface Point {
x: number;
y: number;
}
let p1: Point = {
x: 100,
y: 100
};
let p2 = Point; //错误
// color? 表示该属性是可选的
interface Point {
x: number;
y: number;
color?: string;
}
readonly
来标注属性为只读interface Point {
x: number;
readonly y: number;
}
let p1: Point = {
x: 100,
y: 100
};
p1.y=200 //报错
interface Point {
x: number;
y: number;
color?: string;
// 下面的索引类型也满足了上面color
// 会报错:类型string|undefined属性的color,不能赋值给字符串索引类型number
[prop: string]: number;
}
// 解决办法1
color?:string;
[prop:number]:number
// 解决办法2
color? :number;
[prop:string]:number | undefined;
interface Point {
x: number;
y: number;
[prop: string]: number;
}
interface Point {
[prop1: string]: string;
[prop2: number]: string;
}
interface Point1 {
[prop1: string]: string;
[prop2: number]: number; // 错误
}
interface Point2 {
[prop1: string]: Object;
[prop2: number]: Date; // 正确
}
class Person {
constructor(public username: string) {}
}
class Student extends Person {}
interface Point {
[key: string]: Person;
[key: number]: Student;
}
interface IFunc {
// fn(a:string):string; // 这种写法是错误的,他定义出来的是IFunc的一个方法
(a: string): string;
}
let fn: IFunc = function(a) {}
let fn1: IFunc = function(a){}
interface IFunc{
(x:number,y:number):number
}
let fn1: IFunc = function (a, b) {
return a + b;
};
function todo(callback:IFunc){
let v=callback(1,2)
}
todo(function(a:number,b:number):number{
return a+b;
})
interface IEventFunc{
(e:Event):void
// (e:MouseEvent):void
}
function on(el:HTMLElement,evname:string,callback:IEventFunc){}
let div=document.querySelector('div');
if(div){
on(div,'click',function(e){
e.target
})
}
interface Box {
height: number;
width: number;
}
interface Box {
scale: number;
}
let box: Box = {height: 5, width: 6, scale: 10}
或
的关系function css(ele: Element, attr: string, value: string|number) {
// ...
}
let box = document.querySelector('.box');
// document.querySelector 方法返回值就是一个联合类型
if (box) {
// ts 会提示有 null 的可能性,加上判断更严谨
css(box, 'width', '100px');
css(box, 'opacity', 1);
css(box, 'opacity', [1,2]); // 错误
}
并且
的关系// 对一个对象进行扩展
interface o1 {x: number, y: string};
interface o2 {z: number};
let o: o1 & o2 = Object.assign({}, {x:1,y:'2'}, {z: 100});
// 下面的也可以
let o:o1&o2={
x:100,
y:'a',
z:300
}
有的时候,我们希望标注的不是某个类型,而是一个固定值,就可以使用字面量类型,配合联合类型会更有用
function setPosition(
ele: Element,
direction: "left" | "top" | "right" | "bottom"
) {}
let box = document.querySelector("div");
// ok
box && setPosition(box, 'bottom');
// error
box && setPosition(box, 'hehe');
// 下面这个字面量类型是可以与基础类型等混合在一起的,但是不建议这么做,比如说设置一个'center'
type dir1 = 'left' | 'top' | 'right' | 'bottom' | string;
type dir = 'left' | 'top' | 'right' | 'bottom';
function setPosition(ele: Element, direction: dir) {
// ...
}
这里需要注意一下,如果使用 type 来定义函数类型,和接口有点不太相同
type callback = (a: string) => string;
let fn: callback = function(a) {return 'a'};
// 或者直接
let fn: (a: string) => string = function(a) {}
object
/ class
/ function
的类型interface
自动合并,利于扩展TypeScript
编译器会根据当前上下文自动的推导出对应的类型标注,这个过程发生在
// 自动推断 x 为 number
let x = 1;
// 不能将类型“"a"”分配给类型“number”
x = 'a';
// 函数参数类型、函数返回值会根据对应的默认值和返回值进行自动推断
function fn(a = 1) {return a * a}
断言只是一种预判,并不会数据本身产生实际的作用,即:类似转换,但并非真的转换了
有的时候,我们可能标注一个更加精确的类型(缩小类型标注范围),比如:
let img = document.querySelector('#img');
img
的类型为 Element
Element
类型其实只是元素类型的通用类型src
这个属性是有问题的HTMLImageElement
类型let img = <HTMLImageElement>document.querySelector('#img');
// 或者
let img = document.querySelector('#img') as HTMLImageElement;
let img = document.querySelector("#img");
if (img) {
// img.src // 会报错,因为src不是Element里的通用属性
// 下面两种方法都可以
(<HTMLImageElement>img).src
(img as HTMLImageElement).src
}
this
function fn(a: string): string {return ''};
interface ICallBack {
(a: string): string;
}
let fn: ICallBack = function(a) {return ''};
// 只有这一个是箭头函数
let fn: (a: string) => string = function(a) {return ''};
// 这里也是箭头函数
type callback = (a: string) => string;
let fn: callback = function(a) {return ''};
通过参数名后面添加 ? 来标注该参数是可选的
let div = document.querySelector('div');
function css(el: HTMLElement, attr: string, val?: any) {
}
// 设置
div && css( div, 'width', '100px' );
// 获取
div && css( div, 'width' );
function sort(items: Array<number>, order = 'desc') {}
sort([1,2,3]);
// 也可以通过联合类型来限制取值
function sort(items: Array<number>, order:'desc'|'asc' = 'desc') {}
// ok
sort([1,2,3]);
// ok
sort([1,2,3], 'asc');
// error
sort([1,2,3], 'abc');
剩余参数是一个数组,所以标注的时候一定要注意
interface IObj {
[key:string]: any;
}
function merge(target: IObj, ...others: Array<IObj>) {
return others.reduce( (prev, currnet) => {
prev = Object.assign(prev, currnet);
return prev;
}, target );
}
// 上面的比较复杂了,直接assign即可
function merge(target:IObj,...others:Array<IObj>){
return Object.assign(target,...others);
}
let newObj = merge({x: 1}, {y: 2}, {z: 3});
无论是 JavaScript
还是 TypeScript
,函数中的 this
都是我们需要关心的,那函数中 this
的类型该如何进行标注呢
this
是会随着调用环境的变化而变化的,所以默认情况下,普通函数中的 this
被标注为 any
this
的类型interface T {
a: number;
fn: (x: number) => void;
}
let obj1:T = {
a: 1,
fn(x: number) {
//any类型
console.log(this);
// 下面的也会报错
(<t>this).b
}
}
let obj2:T = {
a: 1,
fn(this: T, x: number) {
//通过第一个参数位标注 this 的类型,它对实际参数不会有影响
console.log(this);
}
}
obj2.fn(1);
this
不能像普通函数那样进行标注this
标注类型取决于它所在的作用域 this
的标注类型interface T {
a: number;
fn: (x: number) => void;
fnn:(x:number)=>void;
}
let obj2: T = {
a: 2,
fn(this: T) {
return () => {
// T
console.log(this);
}
},
fnn(this:Window,x:number){
return ()=>{
// Window
this
}
}
}
有的时候,同一个函数会接收不同类型的参数返回不同类型的返回值,可以使用函数重载来实现
any
)的同名函数noImplicitAny
不能设置为true,会报错function showOrHide(ele: HTMLElement, attr: string, value:
'block'|'none'|number) {
//
}
let div = document.querySelector('div');
if (div) {
showOrHide( div, 'display', 'none' );
showOrHide( div, 'opacity', 1 );
// error,这里是有问题的,虽然通过联合类型能够处理同时接收不同类型的参数,但是多个参数之
间是一种组合的模式,我们需要的应该是一种对应的关系
showOrHide( div, 'display', 1 );
}
看一下函数重载
function showOrHide(ele: HTMLElement, attr: 'display', value: 'block'|'none');
function showOrHide(ele: HTMLElement, attr: 'opacity', value: number);
// 注意上面两个函数是没有{}的
function showOrHide(ele: HTMLElement, attr: string, value: any) {
ele.style[attr] = value;
}
let div = document.querySelector('div');
if (div) {
showOrHide( div, 'display', 'none' );
showOrHide( div, 'opacity', 1 );
// 通过函数重载可以设置不同的参数对应关系
showOrHide( div, 'display', 1 );
}
重载函数类型只需要定义结构,不需要实体,类似接口
interface PlainObject {
[key: string]: string|number;
}
// attr:PlainObject ,这个attr就变成了个对象
function css(ele: HTMLElement, attr: PlainObject);
function css(ele: HTMLElement, attr: string, value: string|number);
function css(ele: HTMLElement, attr: any, value?: any) {
if (typeof attr === 'string' && value) {
ele.style[attr] = value;
}
if (typeof attr === 'object') {
for (let key in attr) {
ele.style[key] = attr[key];
}
}
}
let div = document.querySelector('div');
if (div) {
css(div, 'width', '100px');
css(div, {width: '100px'});
// error,如果不使用重载,这里就会有问题了
css(div, 'width');
}
在类的基础中,包含下面几个核心的知识点,也是 TypeScript
与 EMCAScript2015+
在类方面共有的一些特性
class
关键字constructor
除了以上的共同特性以外,在 TypeScript 中还有许多 ECMAScript 没有的,或当前还不支持的一些特性,如:抽象
通过 class
就可以描述和组织一个类的结构,语法:
// 通常类的名称我们会使用 大坨峰命名 规则,也就是 (单词)首字母大写
class User {
// 类的特征都定义在 {} 内部
}
通过 class 定义了一个类以后,可以通过 new 关键字来调用该类从而得到该类型的一个具体对象:也就是实例化
为什么类可以像函数一样去调用呢,其实执行的并不是这个类,而是类中包含的一个特殊函数:构造函数 - constructor
class User {
constructor() {
console.log('实例化...')
}
}
let user1 = new User;
constructor
不允许有 return
和返回值类型标注的(因为要返回实例对象)通常情况下,会把一个类实例化的时候的初始化相关代码写在构造函数中,比如对类成员属性的初始化赋值
class User {
// 这种方法过于繁琐,见下方public
id: number;
username: string;
constructor(id: number, username: string) {
this.id = id;
this.username = username;
}
postArticle(title: string, content: string): void {
console.log(`发表了一篇文章: ${title}`)
}
}
let user1 = new User(1, 'zMouse');
let user2 = new User(2, 'MT');
在类内部,我们可以通过 this 关键字来访问类的成员属性和方法
class User {
id: number;
username: string;
postArticle(title: string, content: string): void {
// 在类的内部可以通过 `this` 来访问成员属性和方法
console.log(`${this.username} 发表了一篇文章: ${title}`)
}
}
ts
提供了一个简化操作:给构造函数参数添加修饰符来直接生成成员属性public
就是类的默认修饰符,表示该成员可以在任何地方进行读写操作
public
修饰符以后
constructor
外面的id
、username
就不用进行标注了constructor
内的this.id=id
也可以省略protected
、private
、readonly
也会让其变为类的属性,也就是可以通过this
来调用
class User {
constructor(
public id: number,
public username: string
) {
// 可以省略初始化赋值
}
postArticle(title: string, content: string): void {
console.log(`${this.username} 发表了一篇文章: ${title}`)
}
}
let user1 = new User(1, 'zMouse');
let user2 = new User(2, 'MT');
在 ts
中,也是通过 extends 关键字来实现类的继承
class VIP extends User {}
在子类中,我们可以通过 super
来引用父类
constructor
中调用 super()
super(//参数)
,否则会报错super(//参数)
之后才能访问 this
,父类的属性
super
来访问父类的成员属性和方法
super
访问父类的的同时,会自动绑定上下文对象为当前子类 this
class VIP extends User {
constructor(
id: number,
username: string,
public score = 0
) {
super(id, username);
console.log(this.id);
}
postAttachment(file: string): void {
console.log(`${this.username} 上传了一个附件: ${file}`)
}
}
let vip1 = new VIP(1, 'Leo');
vip1.postArticle('标题', '内容');
vip1.postAttachment('1.png');
默认情况下,子类成员方法集成自父类,但是子类也可以对它们进行重写和重载
class VIP extends User {
constructor(
id: number,
username: string,
public score = 0
) {
super(id, username);
}
// postArticle 方法重写,覆盖
postArticle(title: string, content: string): void {
this.score++;
// 这个文档里竟然不认``,确切说是不认${},确切说,不知道什么问题
console.log(`${this.username}发表了一篇文章:${title},积分:${this.score}`)
}
postAttachment(file: string): void {
console.log(`${this.username} 上传了一个附件: ${file}`)
}
}
// 故意多写一个省的下面变色
`
// 具体使用场景
let vip1 = new VIP(1, 'Leo');
vip1.postArticle('标题', '内容');
class VIP extends User {
constructor(
id: number,
username: string,
public score = 0
) {
super(id, username);
}
// 参数个数,参数类型不同:重载
postArticle(title: string, content: string): void;
postArticle(title: string, content: string, file: string): void;
postArticle(title: string, content: string, file?: string) {
// super关键字调用父类方法
super.postArticle(title, content);
if (file) {
this.postAttachment(file);
}
}
postAttachment(file: string): void {
console.log(`${this.username} 上传了一个附件: ${file}`)
}
}
// 具体使用场景
let vip1 = new VIP(1, 'Leo');
vip1.postArticle('标题', '内容');
vip1.postArticle('标题', '内容', '1.png');
有的时候,我们希望对类成员(属性、方法)进行一定的访问控制,来保证数据的安全
通过 类修饰符 可以做到这一点,目前 TypeScript
提供了四种修饰符:
倒不是说就不能通过外面的数据修改内部的属性,比如说protected、private等
class User{
constructor(
private _password:string
){}
set password(password:string){
this._password=password
}
}
let p1=new User('abc')
console.log(p1); // {_password:'abc'}
p1.password='bdc'
console.log(p1); // {_password:'bcd'}
这个是类成员的默认修饰符,它的访问级别为:
修改级别为:
它的访问级别为:
修改级别:
它的访问级别为:
修改级别:
只读修饰符只能针对成员属性使用,且必须在声明时或构造函数里被初始化,它的访问级别为:
修改级别:
class User {
constructor(
// 可以访问,但是一旦确定不能修改
readonly id: number,
// 可以访问,但是不能外部修改
protected username: string,
// 外部包括子类不能访问,也不可修改
private password: string
) {
// ...
}
// ...
protected method(){}
}
let user1 = new User(1, 'zMouse', '123456');
寄存器
来完成这个需求寄存器
,可以对类成员属性的访问进行拦截并加以控制,更好的控制成员属性的设置和访问边界访问控制器,当访问指定成员属性时调用
设置控制器,当设置指定成员属性时调用
class User {
constructor(
readonly _id: number,
readonly _username: string,
private _password: string
) {
}
public set password(password: string) {
if (password.length >= 6) {
this._password = password;
}
}
public get password() {
return '******';
}
// ...
}
this
,那么该方法就是静态的type IAllowFileTypeList = 'png'|'gif'|'jpg'|'jpeg'|'webp';
class VIP extends User {
// static 必须在 readonly 之前
static readonly ALLOW_FILE_TYPE_LIST: Array<IAllowFileTypeList> =
['png','gif','jpg','jpeg','webp'];
constructor(
id: number,
username: string,
private _allowFileTypes: Array<IAllowFileTypeList>
) {
super(id, username);
}
info(): void {
// 类的静态成员都是使用 类名.静态成员 来访问
// VIP 这种类型的用户允许上传的所有类型有哪一些
console.log(VIP.ALLOW_FILE_TYPE_LIST);
// 当前这个 vip 用户允许上传类型有哪一些
console.log(this._allowFileTypes);
}
}
let vip1 = new VIP(1, 'zMouse', ['jpg','jpeg']);
// 类的静态成员都是使用 类名.静态成员 来访问
console.log(VIP.ALLOW_FILE_TYPE_LIST);
this.info();
有的时候,一个基类(父类)的一些方法无法确定具体的行为,而是由继承的子类去实现
现在前端比较流行组件化设计,比如 React
class MyComponent extends Component {
constructor(props) {
super(props);
this.state = {}
}
render() {
//...
}
}
根据上面代码,可以大致设计如下类结构
props
属性,可以通过构造函数进行初始化,由父级定义state
属性,由父级定义render
的方法// 泛型
class Component<T1, T2> {
public state: T2;
constructor(
public props: T1
) {
// ...
}
render(): string {
// ...不知道做点啥才好,但是为了避免子类没有 render 方法而导致组件解析错误,父类就用一个默认的 render 去处理可能会出现的错误
}
}
interface IMyComponentProps {
title: string;
}
interface IMyComponentState {
val: number;
}
class MyComponent extends Component<IMyComponentProps, IMyComponentState> {
constructor(props: IMyComponentProps) {
super(props);
this.state = {
val: 1
}
}
render() {
this.props.title;
this.state.val;
return `组件`;
}
}
父类的 render
有点尴尬(当子类没有定义render的时候,调用render实际走的是父类的render方法,并不是预期的效果),更应该从代码层面上去约束子类必须得有 render
方法,否则编码就不能通过
如果一个方法没有具体的实现方法,则可以通过 abstract
关键字进行修饰
abstract class Component<T1, T2> {
public state: T2;
constructor(
public props: T1
) {
}
public abstract render(): string;
}
TypeScript
只支持单继承,即一个子类只能有一个父类,但是一个类可以实现多个接口契约
在一个类中使用接口并不是使用 extends
关键字,而是 implements
implements
了一个接口,那么就必须实现该接口中定义的契约implements
与 extends
可同时存在interface ILog {
getInfo(): string;
}
class MyComponent extends Component<IMyComponentProps, IMyComponentState> implements ILog {
constructor(props: IMyComponentProps) {
super(props);
this.state = {
val: 1
}
}
render() {
this.props.title;
this.state.val;
return `组件`;
}
// 如果没有这个方法会报错
getInfo() {
return `组件:MyComponent,props:${this.props},state:${this.state}`;
}
}
实现多个接口
interface ILog {
getInfo(): string;
}
interface IStorage {
save(data: string): void;
}
class MyComponent extends Component<IMyComponentProps, IMyComponentState> implements ILog, IStorage {
constructor(props: IMyComponentProps) {
super(props);
this.state = {
val: 1
}
}
render() {
this.props.title;
this.state.val;
return `组件`;
}
getInfo(): string {
return `组件:MyComponent,props:${this.props},state:${this.state}`;
}
save(data: string) {
// ... 存储
}
}
接口也可以继承
interface ILog {
getInfo(): string;
}
// 继承以后IStorage既有getInfo()又有save()
interface IStorage extends ILog {
save(data: string): void;
}
当在 TypeScript 定义一个类的时候,其实同时定义了两个不同的类型
首先,对象类型好理解,就是 new 出来的实例类型
那类类型是什么,我们知道 JavaScript 中的类,或者说是 TypeScript 中的类其实本质上还是一个函数,
当然我们也称为构造函数,那么这个类或者构造函数本身也是有类型的,这个类型就是类的类型
class Person {
// 属于类的
static type = '人';
// 属于实例的
name: string;
age: number;
gender: string;
// 类的构造函数也是属于类的
constructor( name: string, age: number, gender: '男'|'女' = '男' ) {
this.name = name;
this.age = age;
this.gender = gender;
}
public eat(): void {
// ...
}
}
let p1 = new Person('zMouse', 35, '男');
p1.eat();
Person.type;
上面例子中,有两个不同的数据
Person
类(构造函数)Person
实例化出来的对象 p1对应的也有两种不同的类型
Person
)typeof Person
)用接口的方式描述如下
interface Person {
name: string;
age: number;
gender: string;
eat(): void;
}
interface PersonConstructor {
// new 表示它是一个构造函数
new (name: string, age: number, gender: '男'|'女'): Person;
type: string;
}
在使用的时候要格外注意
function fn1(arg: Person /*如果希望这里传入的Person 的实例对象*/) {
arg.eat();
}
fn1( new Person('', 1, '男') ); // 这里的new是上面的class类
// 下面的typeof Person返回的是class类的构造函数
function fn2(arg: typeof Person /*如果希望传入的Person构造函数*/) {
new arg('', 1, '男');
}
// 上面的fn2与下面的fn3相同,写法不同
function fn3(arg:PersonConstructor){
new arg('', 1, '男');
}
fn2(Person);
JavaScript
中通过判断来处理⼀些逻辑TypeScript
中这种条件语句块还有另外⼀个特性:根据判断逻辑的结果,缩⼩类型范围(有点类似断⾔)typeof
可以返回某个数据的类型,在 TypeScript
在 if
、 else
代码块中能够把typeof
识别为类型保护,推断出适合的类型
function fn(a: string|number) {
// error,不能保证 a 就是字符串
a.substring(1);
if (typeof a === 'string') {
// ok
a.substring(1);
} else {
// ok
a.toFixed(1);
}
}
与 typeof
类似的, instanceof
也可以被 TypeScript
识别为类型保护
function fn(a: Date|Array<any>|RegExp) {
if (a instanceof Array) {
a.push(1);
} else if(a instanceof Date){
a.getFullYear();
}else if(a instanceof RegExp){
}
}
in
也是如此
interface IA {
x: string;
y: string;
}
interface IB {
a: string;
b: string;
}
function fn(arg: IA | IB) {
if ('x' in arg) {
// ok
arg.x;
// error
arg.a;
} else {
// ok
arg.a;
// error
arg.x;
}
}
如果类型为字⾯量类型,那么还可以通过该字⾯量类型的字⾯值进⾏推断
interface IA {
type: 'IA';
x: string;
y: string;
}
interface IB {
type: 'IB';
a: string;
b: string;
}
function fn(arg: IA | IB) {
if (arg.type === 'IA') {
// ok
arg.x;
// error
arg.a;
} else {
// ok
arg.a;
// error
arg.x;
}
}
有的时候,以上的⼀些⽅式并不能满⾜⼀些特殊情况,则可以⾃定义类型保护规则
data is Element[]|NodeList
是⼀种类型谓词,格式为: xx is XX
,返回这种类型的函数就可以被 TypeScript
识别为类型保护
function canEach(data: any): data is Element[]|NodeList {
return data.forEach !== undefined;
}
function fn2(elements: Element[]|NodeList|Element) {
if ( canEach(elements) ) {
elements.forEach((el: Element)=>{
el.classList.add('box');
});
} else {
elements.classList.add('box');
}
}
TypeScript
提供了⼀些⽅式来操作类型这种数据,但是需要注意的是,类型数据只能作为类型来使⽤,⽽不能作为程序中的数据,这是两种不同的数据,⼀个⽤在编译检测阶段,⼀个⽤于程序执⾏阶段
在 TypeScript
中, typeof
有两种作⽤
let str1 = 'kaikeba';
// 如果是 let ,把 'string' 作为值
let t = typeof str1;
// 如果是 type,把 'string' 作为类型
type myType = typeof str1;
let str2: myType = '开课吧';
let str3: typeof str1 = '开课吧';
let obj={
name:'aa',
age:30
}
// 用type,a的值是{name:string,age:number}
type a = typeof obj;
// 用let,a的值是Object
let a = typeof obj;
获取类型的所有 key
的集合
集合的形式为 ‘aa’ | ‘bb’ | ‘cc’ | …
interface Person {
name: string;
age: number;
location: string;
}
type K1 = keyof Person; // "name" | "age" | "location"
type K2 = keyof Person[]; // number | "length" | "push" | "concat" | ...
type K3 = keyof { [x: string]: Person }; // string | number
class Person {
name: string = "Semlinker";
}
let sname: keyof Person;
sname = "name";
let K1: keyof boolean; // let K1: "valueOf"
let K2: keyof number; // let K2: "toString" | "toFixed" | "toExponential" | ...
let K3: keyof symbol; // let K1: "valueOf"
type P1 = Person["name"]; // string
type P2 = Person["name" | "age"]; // string | number
type P3 = string["charAt"]; // (pos: number) => string
type P4 = string[]["push"]; // (...items: string[]) => number
type P5 = string[][0]; // string
interface Person {
name: string;
age: number;
};
type personKeys = keyof Person;
// 等同:type personKeys = "name" | "age"
//
let p1 = {
name: 'zMouse',
age: 35
}
function getPersonVal(k: personKeys) {
return p1[k];
}
type a = typeof p1;
// 上面a的值是{name:string,age:number}
function getPersonVal(k:keyof typeof p1){
return p1[k]
}
/**
等同:
function getPersonVal(k: 'name'|'age') {
return p1[k];
}
*/
getPersonVal('name'); //正确
getPersonVal('gender'); //错误
// 另外一个范例
type Todo = {
id: number;
text: string;
done: boolean;
}
const todo: Todo = {
id: 1,
text: "Learn TypeScript keyof",
done: false
}
function prop<T extends object, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
const id = prop(todo, "id"); // const id: number
const text = prop(todo, "text"); // const text: string
const done = prop(todo, "done"); // const done: boolean
针对类型进⾏操作的话,内部使⽤的 for…in
对类型进⾏遍历
下面例子的目的是把age的number类型转化成string类型
也就是说遍历原本的类型,都转化成统一类型
interface Person {
name: string;
age: number;
}
type personKeys = keyof Person;
type newPerson = {
[k in personKeys]: number;
/**
等同 [k in 'name'|'age']: number;
也可以写成
[k in keyof Person]: number;
*/
}
/**
type newPerson = {
name: number;
age: number;
}
*/
注意: in
后⾯的类型值必须是 string
或者 number
或者 symbol
TypeScript
的类型系统是基于结构⼦类型的,它与名义类型(如:java)不同(名义类型的数据类型兼容性或等价性是通过明确的声明或类型的名称来决定的)。这种基于结构⼦类型的类型系统是基于组成结构的,只要具有相同类型的成员,则两种类型即为兼容的。
class Person {
name: string;
age: number;
}
class Cat {
name: string;
age: number;
}
function fn(p: Person) {
p.name;
}
let xiaohua = new Cat();
// ok,因为 Cat 类型的结构与 Person 类型的结构相似,所以它们是兼容的
fn(xiaohua);
interface IFly{
fly():void
}
class Person implements IFly{
name:string;
age:number;
study(){}
fly(){}
}
class Cat{
name:string;
age:number;
catchMouse(){}
}
// 没有定义IFly的时候,这里放Person或者Cat,如果有对应的属性或者方法还好,如果没有就出问题了
function fn2(arg:IFly){
arg.fly();
}
fn2(pp)
// error 没有fly方法
fn2(cc)
许多时候,标注的具体类型并不能确定,比如一个函数的参数类型
function getVal(obj, k) {
return obj[k];
}
所谓的泛型,就是给可变(不定)的类型定义变量(参数), <>
类似 ()
function getVal<T>(obj: T, k: keyof T) {
return obj[k];
}
let obj1={
x:1,
y:2
}
let obj2={
username:'zmouse',
age:35
}
// 这里的typeof obj1实际上等于type obj=typeof obj1 getVal
getVal<typeof obj1>(obj1,'x')
getVal<typeof obj2>(obj2,'age')
在面向对象章节中,我们曾经给大家讲过一个基于泛型使用的例子:模拟组件
abstract class Component<T1, T2> {
props: T1;
state: T2;
constructor(props: T1) {
this.props = props;
}
abstract render(): string;
}
interface IMyComponentProps {
val: number;
}
interface IMyComponentState {
x: number;
}
class MyComponent extends Component<IMyComponentProps, IMyComponentState> {
constructor(props: IMyComponentProps) {
super(props);
this.state = {
x: 1
}
}
render() {
this.props.val;
this.state.x;
return ' ';
}
}
let myComponent = new MyComponent({val: 1});
myComponent.render();
还可以在接口中使用泛型
后端提供了一些接口,用以返回一些数据,依据返回的数据格式定义如下接口:
interface IResponseData {
code: number;
message?: string;
data: any;
}
根据接口,我们封装对应的一些方法
function getData(url: string) {
return fetch(url).then(res => {
return res.json();
}).then( (data: IResponseData) => {
return data;
});
}
但是,我们会发现该接口的 data
项的具体格式不确定,不同的接口会返回的数据是不一样的,当我们想根据具体当前请求的接口返回具体 data
格式的时候,比较麻烦了,因为 getData
并不清楚你调用的具体接口是什么,对应的数据又会是什么样的
这个时候我们可以对 IResponseData
使用泛型
interface IResponseData<T> {
code: number;
message?: string;
data: T;
}
function getData<U>(url: string) {
return fetch(url).then(res => {
return res.json();
}).then( (data: IResponseData<U>) => {
return data;
});
}
定义不同的数据接口
// 用户接口
interface IResponseUserData {
id: number;
username: string;
email: string;
}
// 文章接口
interface IResponseArticleData {
id: number;
title: string;
author: IResponseUserData;
}
调用具体代码
(async function(){
let user = await getData<IResponseUserData>('');
if (user.code === 1) {
console.log(user.message);
} else {
console.log(user.data.username);
}
let articles = await getData<IResponseArticleData>('');
if (articles.code === 1) {
console.log(articles.message);
} else {
console.log(articles.data.id);
console.log(articles.data.author.username);
}
})();
TypeScript
模块化的使用以及与其它模块化系统的差异TypeScript
模块化与其它模块化系统之间的编译与转换TypeScript
模块化进行项目开发模块化是指自上而下把一个复杂问题(功能)划分成若干模块的过程,在编程中就是指通过某种规则对程序(代码)进行分割、组织、打包,每个模块完成一个特定的子功能,再把所有的模块按照某种规则进行组装,合并成一个整体,最终完成整个系统的所有功能
从基于 Node.js
的服务端 commonjs
模块化,到前端基于浏览器的 AMD
、CMD
模块化,再到 ECMAScript2015
开始原生内置的模块化, JavaScript
的模块化方案和系统日趋成熟。
TypeScript
也是支持模块化的,而且它的出现要比 ECMAScript
模块系统标准化要早,所以在 TypeScript
中即有对 ECMAScript
模块系统的支持,也包含有一些自己的特点
无论是那种模块化规范,重点关注:保证模块独立性的同时又能很好的与其它模块进行交互
在早期,对于运行在浏览器端的 JavaScript
代码,模块化的需求并不那么的强烈,反而是偏向 服务端、桌面端 的应用对模块化有迫切的需求(相对来说,服务端、桌面端程序的代码和需求要复杂一些)。CommonJS
规范就是一套偏向服务端的模块化规范,它为非浏览器端的模块化实现制定了一些的方案和标准,NodeJS
就采用了这个规范。
独立模块作用域
一个文件就是模块,拥有独立的作用域
导出模块内部数据
通过 module.exports
或 exports
对象导出模块内部数据
// a.js
let a = 1;
let b = 2;
setTimeout(()=>{
a=20
}.1000)
setTimeout(()=>{
console.log(b); // aaa
},2000)
// or 实际上是把exports这个对象导出去了,那么b.js在require的时候导入的a就是这个exports对象
exports.x = a;
exports.y = b;
// 注意,这个定时器是基于module.exports = exports ,下面module.exports就改变指向了
setTimeout( () => {
module.exports.name = 'bb'
console.log(exports.name); // 'bb'
},3000)
// module.exports = exports 变成了module.exports = {}
module.exports = {
x: a,
y: b
}
导入外部模块数据
通过 require
函数导入外部模块数据
查找顺序:
// b.js
let a = require('./a');
a.x;
a.y;
setTimeout(()=>{
console.log(a.x); // 20
},2000)
setTimeout(()=>{
a.y='aaa'
},1000)
因为 CommonJS 规范一些特性(基于文件系统,同步加载),它并不适用于浏览器端,所以另外定义了适用于浏览器端的规范
异步模块定义:
AMD(Asynchronous Module Definition)
https://github.com/amdjs/amdjs-api/wiki/AMD
浏览器并没有具体实现该规范的代码,我们可以通过一些第三方库来解决
https://requirejs.org/
// 1.html
// data-main加载的是程序的入口文件,也就是第一个JS文件
<script data-main="js/a" src="https://cdn.bootcss.com/require.js/2.3.6/require.min.js"></script>
独立模块作用域
通过一个 define
方法来定义一个模块,在该方法内部模拟模块独立作用域
// b.js
define(function() {
// 模块内部代码
})
导出模块内部数据
通过 return
导出模块内部数据
// b.js
define(function() {
let a = 1;
let b = 2;
return {
x: a,
y: b
}
})
导入外部模块数据
通过前置依赖列表导入外部模块数据
// a.js
// 定义一个模块,并导入 ./b 模块
define(['./b'], function(m2) {
console.log(m2.x); // m2就是上面的对象{x:a,y:b}
})
requireJS
的 CommonJS
风格require.js
也支持 CommonJS
风格的语法
导出模块内部数据
// b.js
define(function(require, exports, module) {
let a = 1;
let b = 2;
// module.exports===exports
// 但是用exports导出的话得是 exports.x=a exports.y=b
module.exports = {
x: a,
y: b
}
})
导入外部模块数据
// a.js
define(function(require, exports, module) {
let b = require('./b')
console.log(b);
})
严格来说,UMD
并不属于一套模块规范,它主要用来处理 CommonJS
、AMD
、CMD
的差异兼容,是模块代码能在前面不同的模块环境下都能正常运行。随着 Node.js
的流行,前端和后端都可以基于 JavaScript
来进行开发,这个时候或多或少的会出现前后端使用相同代码的可能,特别是一些不依赖宿主环境(浏览器、服务器)的偏低层的代码。我们能实现一套代码多端适用(同构),其中在不同的模块化标准下使用也是需要解决的问题,UMD
就是一种解决方式
(function (root, factory) {
if (typeof module === "object" && typeof module.exports === "object") {
// Node, CommonJS-like
module.exports = factory();
}
else if (typeof define === "function" && define.amd) {
// AMD 模块环境下
define(factory);
} else {
// 不使用任何模块系统,直接挂载到全局
root.kkb = factory();
}
}(this, function () {
let a = 1;
let b = 2;
// 模块导出数据
return {
x: a,
y: b
}
}));
// 另外一种写法,帮助理解
// 可以跟上面相互补充
M(function(){
funciton fn1(){
console.log('fn1');
}
return {fn1}
})
(function M(root, factory) {
if (typeof module === "object" && typeof module.exports === "object") {
// Node, CommonJS-like
module.exports = factory();
}
else if (typeof define === "function" && define.amd) {
// AMD 模块环境下
define(factory);
}else{
root.f=factory();
}
}(this.function(){}))
从 ECMAScript2015/ECMAScript6
开始,JavaScript
原生引入了模块概念,而且现在主流浏览器也都有了很好的支持,同时在 Node.js
也有了支持,所以未来基于 JavaScript
的程序无论是在前端浏览器还是在后端 Node.js
中,都会逐渐的被统一
一个文件就是模块,拥有独立的作用域,且导出的模块都自动处于 严格模式
下,即:'use strict'
script
标签需要声明 type="module"
打开的html页面必须用live server
import和export是关键字,不是对象,也不是函数