一、ts开发环境搭建
在node环境中搭建typescript,使用npm指令安装
npm install -g typescript
然后使用tsc就可以使用编译ts文件
1.1 ts默认假设(设置)
默认情况下,ts会做出下面几种假设:
- 假设当前的执行环境是浏览器环境
- 如果代码中没有使用模块化语句(import、export),便认为该代码是全局执行
举个例子
新建一个index.ts文件
bash
let test:string = "123"
执行tsc index.ts,会生成一个index.js文件
然后index.ts文件会报错,test变量不能被重新声明,原因是因为,index.js文件中也有test变量,由于ts默认是全局执行,所以在index.ts中的test变量就不能被重新声明了
- 编辑的目标代码是es3(最大的兼容性)
但是,我们可以手动的改变这些假设
1.2 改变ts默认设置(假设)
- 使用ts命令行时加上参数(不推荐)
- 使用ts配置文件,更改编译选项
二、ts配置文件
使用配置文件后,使用tsc编译时不能更上文件名了,跟上文件名就会忽略配置文件
新建ts配置有两种方式
- 在根目录下手动新建tsconfig.json文件
- 使用命令行的方式
tsc --init
生成tsconfig.json文件
tsconfig.json配置文件有许多的配置
2.1 compilerOptions配置编译选项
perl
{
"compilerOptions":{
"target":"es2016", //js代码标准,默认为es3
"module":"commonjs", //编译目标(编译后的js代码)使用的模块化标准,一般为commonjs(node环境),es6(浏览器环境)
"lib":["es2016"], //ts运行的全局环境,比如es2016代表ts可以使用es2016这个库的环境,但是没有浏览器环境了,像console、document等这些都不再支持了,要解决这个问题就需要安装一个库@types/node。@types是ts官方的类型库,其中包含了很多对js代码的描述,像很多三方库没有ts类型描述,但是又需要类型检测,所有可以去@types中检测是否有。
"outDir":"./dist" //编译后的输出文件目录
},
"include":["./src"], //需要编译的文件
"files":["./src/xx"] //指定编译文件及其依赖文件
}
三、使用三方库简化流程
通过上面的案例知道,ts编译运行需要先执行编译命令,然后再使用node运行文件,并且同时文件变更后有需要重新编译运行,这个相当麻烦,因此有一些三方库来帮助我们简化这些流程。
3.1 ts-node
将ts代码在内存中完成编译,并且完成运行
arduino
npm install -g ts-node //全局安装
运行命令
arduino
ts-node 入口文件 //注意这种方式不会输出编译后文件
3.2 nodemon
用于检测文件变化
全局安装
npm install -g nodemon
scss
nodemon --exec ts-node src/index.ts //该命令的作用是,当文件发生变化就执行后面的命令
添加配置,只要监控ts文件
-e 后面跟需要监控的文件类型
bash
nodemon -e ts --exec ts-node src/index.ts
--watch 后面跟监控的文件夹
css
nodemon --watch src -e ts --exec ts-node src/index.ts
注意tsc有一个命令
tsc --watch
这个命令只会监听编译,但是不会执行编译文件,只要有文件变更就会出编译结果。
四、ts中使用模块化
4.1 新增的tsconfig.json配置
配置名称 | 含义 |
---|---|
module | 设置编译结果中使用的模块化标准 |
moduleResolution | 设置解析模块的模式 |
noImplicitUseStrict | 编译结果中不包含"use strict" |
removeComments | 编译结果移除注释 |
noEmitOnError | 错误时不生成编译结果 |
esModuleInterop | 启用es模块化交互非es模块化导出 |
experimentalDecorators | 开启装饰器语法 |
noImplicitAny | 开启隐式any检查 |
declaration | 编译结果是否生成声明文件 |
4.2 回顾前端模块化标准
4.2.1 ts中如何书写模块化语句?
在es6出来之后,ts就已经完全支持es标准,因此ts中导入导出统一使用es6的模块化标准即可。
4.2.2 编译结果是怎样的?
可在tsconfig.json文件里配置
json
{
"compilerOptions":{
"module":"commonjs" //配置编译目标使用的版本,commonjs、es6...
},
}
注意
- 如果编译结果的模块化标准是es6,则编译结果没有区别
- 如果编译结果是commonjs,导出的声明会变成exports的属性,默认导出会变成exports的default属性
4.3 模块解析
4.3.1 什么是模块解析?
应该从什么位置寻找和解析模块?
4.3.2 模块解析策略
- classic:经典模块解析策略(es6之前就已经出现,已经过时)
- node:node解析策略(后面写node文章补充上)
推荐设置配置moduleResolution:"node"
使用node解析
4.4 默认导出模块报错
在ts文件中直接书用fs这种默认导出模块会报错,比如
java
import fs from "fs"
fs.readFileSync("./") //报错
//编译后文件
const fs_1 = require("fs")
fs_1.default.readFileSync("./") 编译错了,因为fs模块不是用ts写的,fs这个模块使用的module.exports = {}
解决办法
javascript
import {readFileSync} from "fs"
readFileSync("./") //不再报错
//编译后文件
const fs_1 = require("fs")
fs_1.readFileSync("./")
或者
javascript
import * as fs from "fs"
fs.readFileSync("./") //不再报错
//编译后文件
const fs_1 = require("fs")
fs_1.readFileSync("./")
或者
javascript
//1、添加配置 esModuleInterop:true
import fs from "fs"
fs.readFileSync("./") //不再报错
//编译后文件
会生成辅助函数,可自行查看编译结果
小结,即当在开发过程中如果遇到三方库使用的是module.export = {} 方式导出的,则使用上述办法解决
4.5 如何在ts中书写commonjs模块化代码
4.5.1 使用commonjs规范书写
javascript
//a.js文件
module.exports = {
name:"test"
}
//b.js文件
const myModule= require("./a") //myModule 类型是any
上面代码会出现问题,类型检查没了,因此ts提供一种语法解决这个问题
4.5.2 ts中的语法
javascript
//a.js文件
export = {
name:"test"
}
//第一种语法,推荐
//b.js文件导入
//使用es6模块化导入,需要开启esModuleInterop为true
import myModule from "./a" //myModule 类型是{name:string}
//第二种语法
//b.js文件导入
import myModule= require("./a") //myModule 类型是{name:string}
更多的模块化相关的配置可以参考上面
五、ts基本数据类型
5.1 基础数据类型
-
number
-
string
-
boolean
-
array
约束数组类型必须约束数组每一项item的类型
比如数字类型数组
number[]
,字符串数组string[]
等等也可以这样鞋
Array<number>
,同等效果 -
object
这个对象只能约束对象,不能约束对象内部具体的值
-
null 和undefined
这两种类型是其他类型的子类型,它们可以赋值给其他类型
比如
inilet str:string = undefined; str.toUpperCase(); //会报错
为了避免上面情况发生,可以在tsconfig.json中配置ts的严格模式
json{ "compilerOptions":{ "strictNullChecks":true //更加严格的空类型检查 } },
加上
strictNullChecks
后undefined和null就不能赋值给其他类型了,只能赋值给自身。
5.2 其他常用类型
- 联合类型
就是一个变量既可以是一种类型又可以是另一种类型,比如
typescript
let str:string|undefined;
多种类型任选其一,可以配合类型保护进行判断
类型保护:当对某个变量进行类型判断之后,在判断语句块中便可以确定它的确切类型,typeof可以触发类型保护
- void类型
约束函数的返回值,函数不返回任何值
- never类型
通常约束函数的返回值,表示函数永远不可能结束
比如抛出错误
typescript
function throwError(msg:string):never{
throw new Error(msg)
}
function alwaysDoSomething():never{
whild(true){
}
}
- 字面量类型 明确约束变量的值
比如
bash
let age:"18"|"20"; //age 只能取值 "18"和"20"
let arr:[] //这也是字面量约束,表示永远只能取空数组
let obj:{} //这也是字面量约束,表示永远只能取空对象
- 元组类型
表示固定长度的数组,并且数组每一个项的类型都确定
- any类型
绕过类型检查,所以any可以赋值给任意类型
六、扩展类型
6.1 字面量类型的问题
问题一 :字面量在做约束时会产生很多的重复代码(可使用使用类型别名解决)
比如
javascript
let gender:"男"|"女"
gender = "男"
function searchUser(g:"男"|"女"){}
-
类型别名
对已知的一些类型定义名称
typescripttype 类型名称 = 具体类型 type Gender ="男"|"女" let gender:Gender="男" function searchUser(g:Gender){} //定义了一个User类型
问题二:逻辑名称和真实的值产生了混淆,会导致修改真实值的时候产生大量的修改。
问题三:字面量类型不会进入到编译结果
比如
javascript
let gender:"男"|"女"
gender = "男" //想象一下现在取值的地方有很多很多地方
function searchUser(g:"男"|"女"){}
当有一天需要把男变成帅哥,把女变成美女,那改动就大了去了。这就是所谓了把真实值和逻辑值混淆了。使用枚举可以解决。
6.2 枚举
枚举通常用来约束变量的取值范围。
6.2.1 枚举定义语法
ini
enum 枚举名 {
枚举字段1 = 值1,
枚举字段2 = 值2,
...
}
上面的问题可以如下解决
ini
enum Gender {
male = "男",
female = "女"
}
gender = Gender.male //想象一下现在取值的地方有很多很多地方,也没关系了
枚举会参与编译,并且会出现在编译结果中即出现在js中,表现为对象。
6.2.2 枚举的规则
-
枚举的值可以是数字和字符串
数字枚举有一些特点,数字枚举的值会自动自增,比如
typescriptenum Number { one, two, ... //one 的值为 1会自增, .... }
被数字枚举约束的值,可以直接赋值给变量,一般不要这么做
inienum Number { one=1 two, ... //one 的值为 1会自增, .... } let one:Number = 1 //等于 let let one:Number = Number.one one=2
数字枚举和字符串枚举的编译结果有差异
以上面为例子,编译后会出现类似这样的代码
css{ one:1, two:2, 1:"one", 2:"two" }
注意
- 尽量不要在一个枚举中既出现数字又出现字符串
- 使用枚举值,尽量使用枚举字段名称,不要直接使用真实值
6.3 接口与类型兼容
什么是接口?
用于约束类、对象、函数的标准
注意接口不会出现在编译结果中即不会出现在编译后的js文件中。
6.3.1 约束对象
语法
csharp
interface 名称 {
}
这个看起来和类型别名没啥区别,确实在约束对象上它们俩没啥区别,最大的区别在约束类上。建议约束对象使用接口。
6.3.2 约束函数
typescript
interface Test {
(n:number):boolean
}
//等于
type Test = (n:number)=>boolean
//等于
type Test ={
(n:number):boolean
}
6.3.3 接口继承
使用extends
关键字实现
kotlin
interface A {
T1:string
}
interface B extends A {
} //B中有T1
interface C extends B,A {} //可以继承多个
使用type也可以实现类似效果使用&符号,交叉类型,但是更加推荐使用接口的继承。
type的交叉类型和interface的继承有比较大的区别
interface继承不能重新覆盖父interface的成员,而交叉类型是可以的,但是交叉类型会合并相同成员类型交叉
补充 readonly修饰符
只读修饰符
csharp
interface User {
readonly id:string
}
const user:User ={
id:"123"
}
user.id="234" //编译不能通过,因为这个变量为只读形式
只读修饰符也不在编译结果中
6.3.4 类型兼容性
ts中判断类型是否兼容(变量能够被赋值),使用的子结构辩型法:目标类型需要某一些特征,赋值的类型只要满足该特征即可。
- 基本类型判断:完全匹配
- 字面量类型判断:完全匹配
- 对象类型:子结构辩型法
比如
kotlin
interface Parent {
bloodType: "A",
feature:"圆脸"
}
const child = {
bloodType:"A",
feature:"圆脸",
age:18,
swimming:true
}
const parent:Parent = child //赋值成功,因为child有parent的特征
但是如果直接赋值ts会报错,因为使用字面量赋值的时候,ts会进行严格判断
kotlin
interface Parent {
bloodType: "A",
feature:"圆脸"
}
const parent:Parent = {
bloodType:"A",
feature:"圆脸",
age:18,
swimming:true
} //赋值失败,因为ts会进行严格判断
-
函数类型:
参数处理:传递给目标函数的参数可以少不能多
返回值处理:返回值类型一定要匹配,要求返回必须返回,如果不要求返回,则随意
6.4 类
在ts中的属性类需要使用属性列表、比如
kotlin
class User {
constructor(name:string,age:number){
this.name = name; //会报错,因为缺少属性列表描述
this.age = age; //会报错,因为缺少属性列表描述
}
}
正确写法
typescript
class User {
name:string
age:number
constructor(name:string,age:number){
this.name = name;
this.age = age;
}
}
有时可能会写出这种很奇怪的代码
typescript
class User {
name:string
age:number
}
这个User类有name和age的属性列表描述,但是在User中有没有地方使用即这个name和age没有进行初始化,值为undefined。可以通过在tsconfig.json中新增strictPropertyInitialization:true配置,检查是否初始化
6.4.1 设置class属性列表默认值
typescript
class User {
name:string = "zhangsan"
age:number = 18
constructor(name:string,age:number){
}
}
由上可看,可以直接在属性描述列表中赋初值。
6.4.2 属性赋值修饰符
readonly,表示当前属性为只读
typescript
class User {
readonly name:string = "zhangsan"
age:number = 18
constructor(name:string,age:number){
}
}
?,表示当前属性为可选
typescript
class User {
name?:string
age:number = 18
constructor(name:string,age:number){
}
}
6.4.3 访问修饰符
访问修饰符可以控制类中的某个成员的访问权限
- public:默认的访问修饰符,公开的,代表所有代码均可访问
- private:私有的,只有在类中可以访问
- protected:受保护的成员,只能在自身和子类中访问
上面这些修饰符只会存在ts中,编译后的代码是没有的。
ts提供了一种语法糖,当构造函数的参数传入进来被直接赋值的时候,可以使用一种特殊的语法糖
typescript
class User {
name:string
age:number
constructor(name:string,age:number){
this.name = name;
this.age = age;
}
}
等于
typescript
class User {
constructor(public name:string,public age:number){
}
}
//注意一定要加访问修饰符
6.4.4 访问器
作用
控制访问属性的读取和赋值
这里与es2016的语法一样 设置set和get
6.4.5 class作为实例类型的约束
在 TypeScript 中,类(class)可以直接作为类型约束,用于约束变量、泛型参数或函数参数的类型:
-
类作为实例类型的约束
类本身代表其实例的类型,因此可以直接用类名约束变量必须是该类的实例。
typescript
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
}
// 约束变量必须是 Person 实例
function greet(p: Person) {
console.log(`Hello, ${p.name}`);
}
const person = new Person("Alice");
greet(person); // ✅ 合法
greet({ name: "Bob" }); // ❌ 报错:缺少 Person 类的构造逻辑
-
类作为泛型约束
在泛型中,可以通过
T extends Class
的形式约束泛型参数必须是类的实例类型。
scala
class Animal {
makeSound() {
console.log("Some sound");
}
}
function createInstance<T extends Animal>(ctor: new () => T): T {
return new ctor();
}
class Dog extends Animal {
makeSound() {
console.log("Woof!");
}
}
const dog = createInstance(Dog); // ✅ 返回 Dog 实例
dog.makeSound(); // 输出 "Woof!"
-
类的构造函数类型约束
如果需要约束泛型参数是类的构造函数 (而非实例),可以结合
new () => T
使用:
php
function factory<T>(ctor: new () => T): T {
return new ctor();
}
class User {
id = Math.random();
}
const user = factory(User); // ✅ 返回 User 实例
6.5 泛型
谈起泛型先看一个典型的函数场景
ini
function handleArray (arr:any[],n:number):any[] {
if(n >= arr.length) return arr;
const new Arr:any[] = [];
for(let i = 0; i<n; i++){
newArr.push(arr[i]);
}
return newArr
}
const newArr = handleArray([2,3,4,5],2)
上面这个操作数组的案例中,由于在声明handleArray处理方法时不知道用户会出进来怎样的数组,因此只能宽泛约束数组,如果用户传入的是一个字符串数组,也应该进行处理。总而言之就是对于数组的约束力度仍然是不够了。为了更加精确的约束,就需要范型了
泛型定义:是指附属于函数、类、接口、类型别名之上的类型,泛型相当于一个类型变量,只有当调用时才能确定类型
6.5.1 函数中使用泛型
在函数名之后写上 <泛型名称>
上述例子优化
ini
function handleArray<T> (arr:T[],n:number):T[] {
if(n >= arr.length) return arr;
const new Arr:T[] = [];
for(let i = 0; i<n; i++){
newArr.push(arr[i]);
}
return newArr
}
const newArr = handleArray<number>([2,3,4,5],2)
按照上面的语法处理,handleArray在调用的时候就确定了要处理的数组类型了
同时在实际开发中当你使用泛型之后,ts也会很智能的推导数据类型
比如
scss
const newArr = handleArray([2,3,4,5],2)
//不用传入number类型,ts会自动推导出数据类型
6.5.2 泛型默认值
可以在写泛型的地方这样写赋默认值
ini
function handleArray<T = number> (arr:T[],n:number):T[] {
if(n >= arr.length) return arr;
const new Arr:T[] = [];
for(let i = 0; i<n; i++){
newArr.push(arr[i]);
}
return newArr
}
6.5.3 泛型在type、interface、class中的使用
直接在type、interface、class后写上泛型
r
type test1<T> = (n:T,i:T) => boolean
interface test2<T> = {
name:T
}
class Test3<T> {
name:T
}
6.5.4 泛型约束
使用extends约束、比如下面案例
php
interface test1 {
name:string
}
function test2<T = extends test1> (value:T):T{
}
//value中一定有name属性
6.5.5 多范型
很简单就是传入多个范型值
php
interface test1 {
name:string
}
function test2<T = extends test1,K> (value:T,value2:K):T|K{
}
6.6 class的继承
class的继承主要是描述类之间的描述关系,同时解决开发中的重复代码。具体语法和es6一致
子类可以重写父类
-
子类成员不能改变父类成员的类型
scalaclass Test1 { name = "test" } class Test2 extends Test1 { name = 123 // 错误,必须是字符串类型 }
-
无论属性还是方法,子类都可以对父类的相应成员进行重写,但是重写时,需要保证类型的匹配
scalaclass Test1 { name = "test" testfunction(){ console.log("test1function") } } class Test2 extends Test1 { testfunction(){ console.log("test2function") } } const obj = new Test2() obj.testfunction() //test2function
-
注意this的关键字,在继承关系中,this的指向是动态的,调用方法时,根据具体的调用者确定this指向
javascript
class Test1 {
name = "test"
testfunction(){
console.log(`${this.name}`)
}
}
class Test2 extends Test1 {
name = 123
testfunction(){
console.log(`${this.name}`)
}
}
const obj = new Test2()
obj.testfunction() //123
类型匹配
子结构变形法
- 子类对象始终可以赋值给父类
单根性和传递性
单根性:每个类最多只能拥有一个父类 传递性:如果A是B的父类,并且B是C的父类,则可以认为A也是C的父类
6.7 抽象类
为什么需要抽象类? 以大家周知的中国象棋为例,可以根据中国象棋的棋子创建如下对象
scala
class Chess {} //基础棋子对象
class Horse extends Chess {} //马
class Pao extends Chess {} //炮
const horse = new Horse()
const Pao = new Pao()
当代码写多了之后可能会出现以下情况
scala
class Chess {} //基础棋子对象
class Horse extends Chess {} //马
class Pao extends Chess {} //炮
const horse = new Horse()
const Pao = new Pao()
....
const chess = new Chess() //语法没错,但是从面向对象的理解来说是不对的,因为中国象棋中并没有棋子这样的棋子,只有车、马、炮、兵等
为解决上述问题,抽象类诞生。语法,在Chess前面加上 abstract
scala
abstract class Chess {} //基础棋子对象,加上abstract之后表示一个抽象概率,只作为提供子类公共代码,并没有实际意义
class Horse extends Chess {} //马
class Pao extends Chess {} //炮
const horse = new Horse()
const Pao = new Pao()
....
const chess = new Chess() //语法报错,抽象类无法创建实类
有时,某个类只表示一个抽象概率,主要用于提取子类共有成员,而不能直接传健它的对象。该类可以作为抽象类。给类前面加上abstract
,表示该类是一个抽象类,不可创建抽象类对象。
6.7.1 抽象类成员
抽象类中可以有抽象成员,抽象成员只能出现在抽象类中
-
父类中,可能知道有些成员是必须存在的,但是不知道该成员的值或者实现是什么,因此需要一种强约束,让继承该类的子类必须要实现该成员。
scalaabstract class Chess { abstract name:string //抽象属性 abstract getName:()=> void. //抽象方法 } class Horse extends Chess { name = "Horse" //不重写会报错,因为name为抽象成员,必须复写实现 getName(){ console.log("getName") } }
6.8 再学接口(interface)
在上面对接口进行了一次定义,接口用于约束类、对象、函数是一个类型契约。当时并没有去约束类,同时接口与类型别名最大的区别就是接口可以和类进行联用
以一个案例来体会接口与类的联用
有一个马戏团,马戏团中有很多动物;包括:狮子、老虎、猴子、狗,这些动物都有共同的特别,拥有名字,年龄、种类名称。还有一个共同的方法:打招呼,它们各自有各自的技能,技能可以通过训练改变。
马戏团中有以下常见技能
- 火圈表演:单火圈、双火圈
- 平衡表演:独木桥、走钢丝
- 智慧表演:跳舞、算术题
狮子和老虎能进行火圈表演
猴子能进行平衡表演
狗能进行智慧表演
以上背景,先使用面向对象的方式实现一次。不考虑接口的模式
scala
abstract class Animal {
abstract type:string;
constructor(
public name:string,
public age:number
){
}
sayHi(){
console.log("hi")
}
}
class Lion extends Animal {
type = "lion"
singleFire () {
console.log("singleFire")
}
doubleFire () {
console.log("doubleFire")
}
}
class Tiger extends Animal {
type = "tiger"
singleFire () {
console.log("singleFire")
}
doubleFire () {
console.log("doubleFire")
}
}
class Monkey extends Animal {
type = "monkey"
dumoqiao () {
console.log("dumuqiao")
}
zougansi () {
console.log("zougangsi")
}
}
class Dog extends Animal {
type = "dog",
suanshuti () {
console.log("suanshuti")
}
}
上面代码乍一看没问题,但是老虎和狮子有相同的能力,但是实现能力的方式不一样,上面代码没有体现出有相同的能力,比如老虎的跳火圈方法写成singleFire,狮子的跳火圈方法写成Fire,上面的代码也不会报错。
场景案例继续走
动物们登场了
arduino
const animal = [new Lion ("狮子",18),new Tiger ("老虎",19),new Monkey ("猴子",16),new Dog ("狗",22)]
动物们打招呼
javascript
const animal = [new Lion ("狮子",18),new Tiger ("老虎",19),new Monkey ("猴子",16),new Dog ("狗",22)]
animal.forEach(val =>{
val.sayHello()
})
打招呼没问题,因此打招呼的方法继承父类,
但是让多有会进行火圈的动物,完成跳火圈表演,这就不好实现了,因为没有一个标识,表明哪些动物会跳火圈,只能这样写了
scala
const animal = [new Lion ("狮子",18),new Tiger ("老虎",19),new Monkey ("猴子",16),new Dog ("狗",22)]
animal.forEach(val =>{
if(val instanceof Lion){
val.singleFire()
val.doubleFire()
}else if (val instanceof Tiger){
val.singleFire()
val.doubleFire()
}
})
上述代码有严重的隐患,因为判断条件是判断是不是会表演跳火圈,而实际的判断是判断老虎和狮子。当有一天马戏团来了新的狮子,此时狮子不会跳火圈,那这个判断条件问题出现了。
上述问题的根本原因在于
-
对表演的能力没有强约束力
-
容易将类型和能力耦合
系统中缺少对能力的定义 -- 接口
面向对象领域的接口的语意:表达了某个类是否拥有某种能力
使用interface对上面案例进行优化处理
scss
interface IFireShow {
singleFire():void;
doubleFire():void;
}
interface IWisdomShow {
suanshuti():void;
tiaowu():void
}
interface IBalanceShow {
dumuqiao():viod;
zougangsi():viod
}
某个类具有某种能力其实就是实现接口,狮子具备跳火圈的能力
scala
class Lion extends Animal implements IFireShow {
type = "lion"
singleFire () {
console.log("singleFire")
} //不写,写错都会报错
doubleFire () {
console.log("doubleFire")
} //不写,写错都会报错
}
上述问题就解决了对表演的能力没有强约束力。
那么如何判断狮子是否具备有跳火圈的能力呢?使用类型保护处理
期望可以这样判断
scala
const animal = [new Lion ("狮子",18),new Tiger ("老虎",19),new Monkey ("猴子",16),new Dog ("狗",22)]
animal.forEach(val =>{
if(val instanceof IfireShow){
val.singleFire()
val.doubleFire()
}
})
但是这种判断是运行时,所以受限ts在运行时是不参与编译的,所以没法通过上面的方式处理,希望ts以后会优化吧
写个类型保护函数吧
typescript
function hasFireShow(ani:object):ani is IFireShow {
retun ((ani as unknown as IFireShow).singleFire && (ani as unknown as IFireShow).doubleFire)
}
再次优化代码
scala
const animal = [new Lion ("狮子",18),new Tiger ("老虎",19),new Monkey ("猴子",16),new Dog ("狗",22)]
animal.forEach(val =>{
if(hasFireShow(val)){
val.singleFire()
val.doubleFire()
}
})
还是有些戳,没办法,ts现在没有更好的办法,希望以后会优化吧
最后补充
-
接口可以继承类,表示该类的所有成员都在接口中
kotlinclass Test1 { a = " " b = " " } class Test2 { a1 = " ", b1 = " ", } interface Test3 extends A,B { } // 相当于 interface Test3 { a:string b:string a1:string b1:string }
七、装饰器
装饰器是面向对象的概念,在java中叫注解,在c#中叫特征,在ts中称为装饰器decorator,在angular中大量使用,目前js是支持装饰器的,但是处于建议阶段。
7.1 解决的问题
- 分离关注点 以一个背景为例,创建一个用户类,
c
class User {
id:string
}
现在有个需要需要验证用户的id不能大于10位数,第一种做法,创建一个验证函数
typescript
class User {
id:string
...
}
const user = new User()
const validateUser(u:User){
if(user.id?.length<=10){
...
}
}
上面代码看起来没什么问题,但是换种场景就会发现有一些问题了,比如现在有两位同事进行开发,a同事开发了User这个类,b同事使用这个类,那么a同事一定是更加清楚这个id的使用验证规则的。所以上面代码还是不够清晰,好的有些同学可能要又会想到,可以在class 中书写
typescript
class User {
id:string,
validateUser(){
if(this.id?.length<=10){
...
}
}
...
}
const user = new User()
user.validateUser()
上面代码看起来也没什么问题,但是细想,id这个属性还是和校验方法分开的当属性多了之后也是很烦的。
那么能不能在最开始定义这个id的时候就把校验规则加上呢?这种思维模式就是关注点的问题:在定义某些东西时,应该是最清楚该东西的情况的?
- 重复代码问题
还是以上述为例,检验规则可能是5位也可能是15位等等,就会出现很多的重复if代码
上面两个问题产生的根源是,某些信息在定义时,能够附加的信息有限。 使用装饰器就可以为一些属性添加额外的信息
less
class User {
@required
@range(5,15)
id:string,
validateUser(){
if(this.id?.length<=10){
...
}
}
...
}
通过上面的代码就知道了,装饰器的作用,为某些属性,类,参数,方法提供元数据信息。
7.1.1装饰器的本质
在js中装饰器就是一个函数。装饰器是会参与运行的。
装饰器可以修饰:
- 类
- 类成员(属性+方法)
- 参数
7.2、类装饰器
类修饰器的本质也是一个函数,该函数接收一个参数,表示类本身(构造函数本身) 注意在ts中要使用装饰器,需要在tsconfig.json中开启experimentalDecorators
装饰器函数运行时间,在类定义后直接运行 语法
typescript
@test1
class Test {
constructor() {}
}
function test1(target: new () => object) {
console.log(target);
}
上述代码编译后
javascript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
let Test = class Test {
constructor() { }
};
Test = __decorate([
test1
], Test);
function test1(target) {
console.log(target);
}
约束函数参数为类的ts
new ()=>object
约束class没有参数的类new (...args:any[])=>object
约束class有参数的类
类装饰器的返回值
- void:仅运行函数
- 返回新的类,会将新的类替换掉装饰器的目标类
多装饰器调用顺序是从下到上
7.3成员装饰器
成员装饰器分为属性和方法
7.3.1 属性装饰器
属性装饰器本质也是一个函数,该函数需要两个参数,
参数一
- 如果是静态属性参数为类本身
- 如果是实例属性,则为类的原型
参数二
- 属性名
7.3.2 方法装饰器
方法装饰器本质也是一个函数,该函数需要三个参数,
参数一
- 如果是静态方法,则为类本身
- 如果是实例方法,则为类原型
参数二
- 固定为一个字符串,表示方法名
参数三
- 属性描述符对象
7.4 reflect-metadata库
作用,在装饰编写代码的过程中,如果需要存入一些数据,一些开发者可能会存在对象的原型上,这样就会污染原型,因此引入reflect-metadata用来外部存储这些数据
具体使用参考官方文档,比较简单
7.5 class-validator 和 class-transformer库
class-validator:对类做一些验证,不成功则返回错误数据
class-transformer:把平面对象转换成class对象
7.6 装饰器补充
7.6.1 参数装饰器
使用场景,依赖注入设计模式中使用
要求函数有三个参数:
-
参数一
- 如果方法是静态的,则为类本身;如果方法是实例方法,则为类的原型
-
参数二
- 方法名称
-
参数三
- 参数列表索引
less
class Test {
sum (a:number,@numberTest b:number) {
}
}
function numberTest (target:any,method:string,index:number) {
}
依赖注入设计模式,后续一篇文章讲解
7.6.2 自动注入元数据
元数据:描述数据的数据
如果安装了reflect-metadata
,并且导入了该库,并且在某个成员上添加了元数据。并且在tsconfig.json中启用了emitDecoratorMetadata
则ts在编译结果中,会将约束的类型,作为元数据加入到相应位置
这样一来ts的类型约束可以支持运行时了
目前使用场景很少、未来可能会发生大变化
7.6.3 AOP
属于面向对象的一种编程方式,将一些在业务中出现的功能块横向切分,分离关注点
八、索引器
css
const obj = {
a:123
}
console.log(obj["a"])
这种读取对象值使用[]
方式读取值的方式就是类型表达式
注意
ts中不会对类型表达式做严格的类型检查
arduino
class Test {
}
const test = new Test()
test["a"] //不会报错,因此ts不会对类型表达式做严格的类型检查
如果期望做严格类型检查需要在tsconfig.jons中开启一个配置noImplicitAny:true
使用索引器代码修改
typescript
class Test {
[prop:string]:any //它会限制当前类的所有属性
}
test["a"] //也不会报错了
在索引器中键可以是数字(数组)和字符串
索引器的作用
- 可以在严格的检查模式下,实现类动态增加成员
- 可以实现动态操作类成员
九、类型计算
根据已知信息,计算新的类型
9.1 三个关键字
typeof
ts中的typeof书写位置在类型约束的位置上,获取某个数据类型比如
ini
const a = "123"
const b:typeof a ="2" //会报错,因为 typeof a 会得到一个字面量类型 '123',所以b只能是 "123"
注意当typeof主用于类(class)时,得到的是其构造函数
keyof
作用于class,interface,type用于获取其他类型的中成员组成的联合类型,比如
typescript
interface User {
id:string,
age:number
}
keyof User // id:string | age:number
in
该关键字一般和keyof联用,限制某个索引的取值范围,比如
go
type Obj = {
[p in "id"|"age"]:string
}
//等于
type Obj = {
id:string,
age:string
}
const obj:Obj ={
id:"123",
age:"123",
hah:"123" //会报错
}
有了上述的三个关键字,就可以编写一些ts类型工具了
9.2 常见工具类型
Partial<T>
把范型T
的所有属性都变成可选的
r
type Partial<T>={
[p in keyof T]?:T[p]
}
Required<T>
把范型T
的所有属性都变成必选的
r
type Required<T>={
[p in keyof T]-?:T[p]
}
ReadOnly<T>
把范型T
的所有属性都变成只读的
r
type ReadOnly<T>={
readonly [p in keyof T]:T[p]
}
String<T>
把范型T
的所有属性都变成字符串
typescript
type String<T>={
[p in keyof T]:string
}
Pick<T, K>
css
type Pick<T,K> = {
[p in keyof T] : K[p]
}
从 T
中选取一组属性 K
构成新类型。
十、声明文件
10.1 什么是声明文件
以.d.ts
结尾的文件
10.2 声明文件的作用
为js代码提供类型声明
10.3 声明文件的位置
- 放置到tsconfig.json包含的目录中
- 放置到node_modules/@types文件目录中,一般都是npm安装的地方
- 手动配置,在tsconfig.json文件中typeRoots中目录配置,这个配置之后,前面两种规则失效
- 与js代码所在目录相同,并且文件名也相同的文件,这种格式其实就是ts工程发布之后的格式
10.4 编写声明文件
-
手动编写
手动编写的场景
- 已有的库,它是使用的js书写而成,并且更改该库代码为ts成本较高
- 对一些第三方的库,它们使用js写的,并且这些三方库没有提供声明文件,可以手动编写声明文件
-
自动生成
- 工程如果是使用ts生成的,发布编译后的js文件,如果发布的文件需要被别人使用,别人也需要类型检查,那么此时可以使用声明文件来描述发布结果结果中的类型
- 此时ts工程就可以使用自动生成,在
tsconfig.json
中配置declaration即可
全局声明
typescript
declare var console {
log:(message?:any)=>void
}
namespace:表示命名空间,可以将其认为是一个对象,命名空间中的内容必须通过命名空间.成员名访问
typescript
declare namespace console {
log:(message?:any)=>void
}
十一、tslint
tslint和eslint差不多,tslint用来检查ts的代码规范的
一般安装到工程内
npm install - D tslint typescript
初始化类型检查配置文件
csharp
npx tslint --init
可以根据官网配置相应的检查规则