大势所趋:2020 全面拥抱TypeScript

656 阅读29分钟

前言

js写多了就更能体会到静态类型检查的优势和魅力,编译时检查总比运行时检查要好得多。目前所在公司团队内并未使用ts,但总要有第一个吃螃蟹的人去推动事情向着更好的方向发展,冲冲冲...


接下来,正文开始

安装和环境搭建

1. 全局安装typescript

npm install -g typescript

2. 项目初始化

一般而言,ts都是编译成js后再执行js,如果想一步到位,可以安装ts-node

  • 新建目录(项目名可自定义):mkdir ts-dev
  • 进入项目文件夹ts-dev: cd ts-dev
  • 生成package.json文件: npm init -y
  • 生成tsconfig.json文件: tsc --init
  • 本地安装ts和ts-node:npm i typescript ts-node

3. package.json中配置编译和运行脚本

"scripts": {
    "build": "tsc -w",  // 编译ts为js,参数-w表示实时监听文件变化
    "start": "ts-node ./src/index.ts" //一步到位的运行ts代码
  }

4. tsconfig.json常用配置

{
  "compilerOptions": {
    "target": "es5",                            // 指定 ECMAScript 目标版本: 'ES5'
    "module": "commonjs",                       // 指定使用模块: commonjs/amd/system/umd/es2015
    "moduleResolution": "node",                 // 选择模块解析策略
    "experimentalDecorators": true,             // 启用实验性的ES装饰器
    "allowSyntheticDefaultImports": true,       // 允许从没有设置默认导出的模块中默认导入。
    "sourceMap": true,                          // 编译后同时生成对应的 map路径映射文件
    "strict": true,                             // 启用所有严格类型检查选项
    "noImplicitAny": true,                      // 在表达式和声明上有隐含的 any类型时报错
    "alwaysStrict": true,                       // 以严格模式检查模块,并在每个文件里加入 'use strict'
    "declaration": true,                        // 生成相应的.d.ts文件
    "removeComments": true,                     // 删除编译后的所有的注释
    "noImplicitReturns": true,                  // 不是函数的所有返回路径都有返回值时报错
    "importHelpers": true,                      // 从 tslib 导入辅助工具函数
    "lib": ["es6", "dom"],                      // 指定编译中的库文件,部分es6新特性需要依赖es6lib
    "typeRoots": ["node_modules/@types"],
    "outDir": "./dist",                           //编译后出口
    "rootDir": "./src",                           //根目录入口
    "noEmitOnError":true                          //编译不通过不生成编译后文件,默认会生成
  },
  "include": [                                  // **/ 表示递归匹配任意子目录
    "./src/**/*.ts"
  ],
  "exclude": [
    "node_modules",
    "dist",
    "**/*.test.ts",
  ]
}

5. hello world

  • ts-dev下新建src文件夹: mkdir src
  • src下新建index.ts: cd src && touch index.ts
  • 在index.ts 中写入如下代码
//ts 中,使用冒号: 指定变量的类型,冒号 :的前后有没有空格都可以。
// 注意:函数有参数约束和返回值约束两种约束
function sayHello(person: string):string {
    return 'Hello, ' + person;
}

const user = "冷月心"
console.log(sayHello(user));//冷月心
  • 编译 :npm run build
  • 项目文件夹下会出现一个dist文件夹
  • 编译后的文件dist/index.js

dist/index.js

"use strict";
function sayHello(person) {
    return 'Hello, ' + person;
}
var user = "冷月心";
console.log(sayHello(user));
//# sourceMappingURL=index.js.map

6. noImplicitAny属性配置测试

tsconfig.json中我们配置noImplicitAny为true,这意味着在表达式和声明上有隐含的 any类型时报错。这其实是一个严谨的编码习惯,anyScript会削弱静态类型检查的价值。

  • 亲测:函数返回值的any检测不到,书写时候要注意

  • 修改src/index.ts如下

function sayHello(person) {
    return 'Hello, ' + person;
}

const user = "冷月心"
console.log(sayHello(user));
  • 编译失败:Parameter 'person' implicitly has an 'any' type.

7. noEmitOnError属性配置测试

默认情况下,即使编译不通过也会生成编译后文件。设置noEmitOnError属性为true,上述操作6隐含any类型导致的编译不通过,就不会生成编译后文件

基础类型

ts基础类型分为两种:原始数据类型和对象类型,其中原始数据类型包括:布尔值、数值、字符串、null、undefined 以及 es6系列中的 symbol,bigint。

1. 布尔/数值/字符串

const bol: boolean = true;
const num: number = 1024;
const userName: string = "冷月心";
//字符串模板依旧能用
const str:string=`${userName}`

2. null和undefined

//这两个类型只有 自己
const onlyNull: null = null;
const onlyUndefined: undefined = undefined;

3. function

声明的函数在调用的时候,参数个数要和声明时候的参数个数保持一致


//没有返回值的函数可以用void声明
const fn1 = (param1:string,param2:number): void => {
  console.log("我是没有返回值的箭头函数");
};

function f2(param1:string,param2:number):void{
  console.log("我是没有返回值的普通函数");
}

//有返回值的箭头函数声明是这样的
const fn3 = (): string => {
  return "冷月心=>"
};

//有返回值的普通函数声明是这样的

function f4():string{
   return "冷月心fn"
}


//函数表达式的双向限定
//上述fn1其实只对=右侧做了限制,对左侧并没有
//完善一点,可以这样 => 用来表示函数的定义,左输入类型,需要用括号括起来,右输出类型

const fn1:(param1:string,param2:number)=>void = (param1:string,param2:number): void => {
  console.log("我是没有返回值的箭头函数");
};

// 函数的可选参数
// 注意可选参数要在确定参数后
function f5(name:string,age?:number):string{
   return "冷月心fn"
}

//函数参数默认值
function f6(name:string,age:number=18):string{
   return `${name}--${age}`
}
//此时可选参数不必一定在确定参数后,但是调用有问题
function f7(name:string,desc?:string,age:number=18):string{
   return `${name}--${age}--${desc}`
}

//rest参数
function f8(...args:number[]):number[]{
  return args
}
console.log(f8(1,2,3,4,5))//[1,2,3,4,5]

4. void声明变量

void也可以用来声明变量,但只能作用在undefined身上,null也不行。只有tsconfig.json中strictNullChecks属性设置为false时,null才能赋值给void

const u:void=undefined;//这是个鸡肋用法,基本不会这么用
const n:void=null;//这样会报错
const age:void=18;//这样也会报错

5. symbol

symbol使用依赖es6编译辅助库 ,tsconfig.json lib["es6"]

const sym1:symbol = Symbol();
const sym2:symbol = Symbol();
console.log(sym1===sym2)//false

6. bigint

bigint可以安全地存储和操作大整数, 目前兼容性不是很好


// 超出最大整数的计算会超精度,得不到期望值 

const max = Number.MAX_SAFE_INTEGER;
const max1 = max + 1
const max2 = max + 2
console.log(max1===max2)// true ,实际应该不相等


// 需要将数值转成BigInt计算,不会超精度,以下为js代码
const big_max = BigInt(Number.MAX_SAFE_INTEGER);
//这里n是bigint的标志,且bigint和number是完全不同的两个类型
//ts中使用bigint,不支持1n,2n,但是可以BigInt(1),BigInt(2)代替
const big_max1 = big_max + 1n
const big_max2 = big_max + 2n
console.log(big_max1 === big_max2) // false 这才是预期结果

任意值

任意值(any/unknown)用来表示允许赋值为任意类型

  • 声明一个变量为任意值之后,对它的任何操作,返回的内容的类型都是任意值。
  • 变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型(隐式,noImplicitAny属性可以校验到)
  • 了解后分分钟忘了即可,这不是什么好习惯

1. any

//非any类型不可以跨类型赋值
let str1:string="冷月心";
str1=123//报错

//any类型可以跨类型赋值,这就像js本身
//注意,这种属于显式声明any类型,tsconfig.json中noImplicitAny属性校验的是隐式
let str2:any="冷月心"
str2=123//不报错

2. unknown

//any类型下,可以在任意值上访问任何属性,调用任何方法,但是unknown不行

let str3:any="冷月心"
str3.say();//不报错
str3.age //不报错

//unknown是更安全的any
let str4:unknown="冷月心"
str4.say();//报错
str4.age //报错

数组

如果你学过java,那你应该更容易理解ts中数组的两种定义方式,选个喜欢的就好

//像java中的集合+泛型,List<String> arr=new ArrayList();
const arr1: Array<string> = ["冷","月","心"] 
//类型后+[],java中也是如此:String [] arr = {"冷","月","心"};
const arr2: string[] = ["冷","月","心"]

//数组的项中不允许出现其他的类型
const arr3: string[] = ["冷","月","心",1024]//报错 1024非字符串

元组

ts中的元组和js的数组很像,可以存储不同类型的值

1. 基本使用

元组使用过程中要保证元组类型定义的顺序和填充类型的顺序一致,数量一致,可以按索引访问


const tuple1: [string, number]=["冷月心",24]//这样是ok的

//按索引访问,其实编译后就是js数组 ["冷月心", 24]
console.log(tuple1[0])//冷月心

//这样会报错
const tuple2: [string, number]=[24,"冷月心"]//顺序不一致
const tuple3: [string, number]=["冷月心"]//缺少
const tuple4: [string, number]=["冷月心",24,25]//多余

2. 元组越界

ts允许向元组中使用数组的push方法插入新元素(但不允许访问)

const tuple5: [number, number] = [1, 2];
tuple5.push(3); // 正常运行
console.log(tuple5); //  正常运行 [1,2,3]
console.log(tuple5[2]); //访问新插入的元素会报错

枚举

枚举类型可以由枚举值得到它的名字,这种感觉像对象的键值对。

  • 枚举类型也确实属于对象类型
  • ts只支持基于数字和字符串的枚举
  • 对于基于数字的枚举类型支持键值对的反向映射 key<=>value
  • 基于字符串的不可以反向映射?当然不可以,就是纯js对象
//这样不行
enum Flag{
  open=true,
  close=false
}

1.数字枚举-默认增长

当我们不在乎成员的值的时候,这种自增长的行为是很有用的,但是要注意每个枚举成员的值都是不同的。

enum Language{
  java,
  node,
  php,
  python
}

//可以按值访问
console.log(Language[0])//java
//也可以按键访问
console.log(Language["java"])//0

//打印结构如下
console.log(Language);

{
  '0': 'java',
  '1': 'node',
  '2': 'php',
  '3': 'python',
  java: 0,
  node: 1,
  php: 2,
  python: 3
}

//编译后代码如下 IIFE传参的形式
"use strict";
var Language;
(function (Language) {
    Language[Language["java"] = 0] = "java";
    Language[Language["node"] = 1] = "node";
    Language[Language["php"] = 2] = "php";
    Language[Language["python"] = 3] = "python";
})(Language || (Language = {}));
console.log(Language);
//# sourceMappingURL=index.js.map

//可以看到内层赋值生成的是
{
  java: 0,
  node: 1,
  php: 2,
  python: 3
}
//外层赋值生成的是
{
  '0': 'java',
  '1': 'node',
  '2': 'php',
  '3': 'python',
}

2.数字枚举-自定义增长

当我们需要成员的值按照我们预期设定的的时候,就需要手动设置枚举的增长值。当然,未设置的值会根据上下文增长.

enum Language{
  java=5,
  node,
  php,
  python
}

//打印一下
console.log(Language)

{
  '5': 'java',
  '6': 'node',
  '7': 'php',
  '8': 'python',
  java: 5,
  node: 6,
  php: 7,
  python: 8
}

//当然你也可以全部手动指定每个值,不连续也可以

enum Language{
  java=100,
  node=101,
  php=103,
  python=104
}

3. 字符串枚举

其实就是把上述数字枚举的值换成字符串,但结构有些不同,再次证明,枚举属于对象类型

enum Language{
  java="J",
  node="N",
  php="P",
  python="PY"
}

//打印一下
console.log(Language)

{ 
 java: 'J',
 node: 'N', 
 php: 'P', 
 python: 'PY' 
}


//看看编译后的js

"use strict";
var Language;
(function (Language) {
    Language["java"] = "J";
    Language["node"] = "N";
    Language["php"] = "P";
    Language["python"] = "PY";
})(Language || (Language = {}));
console.log(Language);
//# sourceMappingURL=index.js.map

4. 异构枚举

这不是新类型,属于衍生类型,包含数字和字符串组合的枚举

enum SwitchEnum {
    open = 1,
    close= "0",
}

//编译后
"use strict";
var SwitchEnum;
(function (SwitchEnum) {
    SwitchEnum[SwitchEnum["open"] = 1] = "open";
    SwitchEnum["close"] = "0";
})(SwitchEnum || (SwitchEnum = {}));
//# sourceMappingURL=index.js.map

对象

ts在对象的使用上有着一些限制,必须是特定类型的实例,常常配合interface使用。

  • interface定义的属性分隔用分号/逗号/空着 都可以

1. 基本使用


//创建一个空对象
const obj:object={};

//也许你想这样给对象增加属性
//但是不行,会报错,因为最初是一个空对象,自然没有这个name属性
obj.name="tom"

//这就需要好用的interface出场了
//如果你用过java,那应该清楚接口是一套待实现的规范
//和元组规范类似,实例属性与声明类型的属性数量和名称严格一致,不可多不可少
interface User{
  name:string
}

const u1:User={
  name:"冷月心"
}
//报错,多属性
const u2:User={
  name:"冷月心",
  age:24
}
//报错,少属性
const u3:User={}

//若属性值为函数,也需要在接口中声明
interface Person{
  name:string,
  say: (something: string) => string
}

const p:Person={
  name:"jack",
  say: (something: string) => something
}

console.log(p.say("hello"))//hello

2. 可选属性

有些时候我们希望某些属性是可选的,使用?修饰

interface Stu{
  name:string,
  desc?:string
}

//此时缺少desc属性也是可以的,因为它是可选属性
const s:Stu={
  name:"tom"
}

3. 任意属性

有些时候我们允许任意属性的添加,但是其使用和表现上有几个要注意的点

interface Stu {
    name: string;
    sno?: number;
    [prop: string]: any;
}

//这样写ok
const s1: Stu = {
    name: 'Tom',
    sex: '男'
};
//这样写也ok,可选属性没什么影响
const s2: Stu = {
    name: 'Jack',
    sex: '男',
    sno:10010
};


3.1 [prop: string]: any;虽然只写了一次,但是任意属性可以不止一个

const s: Stu = {
    name: 'Join',
    sex: '男',
    sno:10010,
    className:"A班",
    desc:"热爱编程"
};

3.2 [prop: string]: any; 任意属性是any,不是仅限于string

//这些都没问题,正常运行
const s: Stu = {
    name: 'Join',
    sex: '男',
    sno:10010,
    className:"A班",
    desc:"热爱编程",
    hobit:["吃饭","睡觉","打豆豆"],
    soulmate:{},
    flag:true
};

3.3 基数索引

[prop: string]: any; 也许你觉得[prop:string]是固定写法,其实prop只是个自定义变量,你可以简写成p,甚至a,b,c,d,但是只有[prop: string],[prop: number] 这两种写法

  • [prop: number]并不意味着属性值可以是number或any??
interface Stu {
    name: string;
    [prop: number]: any;
}

//以下皆报错
const s1: Stu = {
    name: 'Jack',
    sex: '男',
};

const s2: Stu = {
    name: 'Jack',
    age: 18,
};

const s3: Stu = {
    name: 'Jack',
    hobit:["吃饭","睡觉","打豆豆"],
};
const s4: Stu = {
    name: 'Jack',
    soulmate:{},
};
const s5: Stu = {
    name: 'Jack',
    flag:true
};

//好的,关键点来了
//[prop:number]:any,意味着属性名必须是数字,值可以任意,以下都ok

const s6: Stu ={
  name:"jack",
  0:true,
  1:[],
  2:{},
  3:"冷月心",
  4:Symbol()
}
   

4. 对象类型的子类型

//看看对象类型的子类型
enum Like {
   coding
}

const arr:number[]=[1,2,3];
const tuple:[string,number]=["冷月心",1024]
const fn=():void=>{} 

//之前我们说过,ts不允许跨类型赋值(非any)
//以下代码都会正常运行,这就意味着它们都是对象子类型
obj=Like;
obj=arr;
obj=tuple;
obj=fn;

5. 只读属性

有时我们希望对象某些属性自赋值后不被更改,使用readonly修饰即可

interface Stu {
    readonly sno:number
}

//学生学号自赋值后不允许更改
const s:Stu={
  sno:10010
}

s.sno=10011 //报错 Cannot assign to 'sno' because it is a read-only property.

类型推论

如果没有明确的指定类型,那么ts会依照类型推论的规则推断出一个类型

1. ts更严格

虽然ts是js超集,但并不意味着所有js的用法都能照搬,js可以跨类型给变量赋值,但是ts不行。

//这种写法ts会报错,js是可以的
let str="lengyuexin";
str=1024

2. ts推论结果

ts根据第一次变量赋值的类型进行推导,实际上,上述代码和下边代码是等价的

//这个string类型就是ts根据第一次变量赋值的类型进行推导得到的结果
let str:string="lengyuexin";
str=1024

3. 定义但不赋值

如果变量定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any 类型而完全不被类型检查


//这不是什么好习惯,像极了脱缰的野马
//当你试图削弱ts静态类型检查优势的时候,说明你依旧过度依赖js写法
let str;
str="lengyuexin"
str=1024;
str=[]
str=false;
str={}

联合类型

联合类型表示取值可以为多种类型中的一种,不同类型使用管道符|分隔,有点像或的感觉。

1. 使用示例

let age:string|number;
age=18;//ok
age="18"//ok
age=false//error  ,非联合类型子类型的不可以

2. 访问联合类型的属性和方法

这有点像交集,只有共有的属性和方法才可以被访问

//这样代码会报错,number类型是没有length属性的
function getLength(something: string | number): number {
    return something.length;
}
//但是这样就可以,字符串和数组都有length属性
function getLength(something: string | number[]): number {
    return something.length;
}

//也可以像这样访问公共方法
function getStr(something: string | number[]): string {
    return something.toString();
}

3. 联合类型的类型推论

类型推论的规则对于联合类型依旧适用

let age: string | number;
age = '20';
console.log(age.length); // 正常运行
age = 20;// 第一次赋值,age已经被ts视为string
console.log(age.length); // Property 'length' does not exist on type 'number'.

类型断言

类型断言(Type Assertion)可以用来手动指定一个值的类型,ts中的断言有以下两种使用方式:

  1. <类型>值
  2. 值 as 类型 在tsx(React jsx的ts版)语法中必须用这种

1. js中的断言

console.assert(expression, message),expression为false情况下,会在控制台打印message

console.assert(true,"hello")//正常运行,无输出
console.assert(false,"hello")//VM2587:1 Assertion failed: hello

2. ts中的断言

联合类型的变量尚未确定类型的时候,只能访问联合子类型共有的属性或方法,断言可以突破这个限制


//这样会报错
function getLength(something: string | number): number {
    if (something.length) {
        return something.length;
    } else {
        return something.toString().length;
    }
}

//用断言就ok

function getLength(something: string | number): number {
  if ((<string>something).length) {
      return (<string>something).length;
  } else {
      return something.toString().length;
  }
}

//类型断言不是类型转换,断言成一个联合类型中不存在的类型是不允许的
// 这样会报错,boolean不属于联合类型string | number的子类型
function toBoolean(something: string | number): boolean {
    return <boolean>something;
}

内置对象

ts核心库定义文件定义了所有浏览器环境需要用到的类型,内置类型判断,NodeJS的需要安装@types/node

1. esma

Boolean、Error、Date、RegExp 等。

let b: Boolean = new Boolean(1);
let e: Error = new Error('Error occurred');
let d: Date = new Date();
let r: RegExp = /[a-z]/;
//...

2. bom/dom

Document、HTMLElement、Event、NodeList 等。

let body: HTMLElement = document.body;
let allDiv: NodeList = document.querySelectorAll('div');
document.addEventListener('click', function(e: MouseEvent) {
  // Do something
});

3. 内置类型检查

//报错 e被推断成 MouseEvent,而 MouseEvent 没有 targetCurrent 属性
document.addEventListener('click', function(e) {
    console.log(e.targetCurrent);
});

类与接口

上文对对象类型进行约束的时候使用了接口,实际上,接口还可以用于对类的某个行为进行抽象。而至于类,可用于对接口的特定规范进行实现,也就是常说的某接口的实现类。类可以继承(单继承),接口也可以(多继承),面向对象的思想在ts中体现的更明显。

1. 类实现接口

接口可以是类某一行为的抽象,类可以实现多个接口。将动物视为一个类,哺乳动物为动物的子类。猫和狗都属于哺乳动物,都具有发出叫声这一行为。而这一行为,就可以抽象为接口。

//定义动物类 若有实例属性外部初始化,需要构造函数
class Animal {
  color: string;
  constructor(color: string) {
    this.color = color;
  }
  //也可以声明实例方法
  yell(str:string):string{
    return str
  }
}


//*******************//

// 以下示例基于最原始的 class Animal {}


//将叫声这一行为抽象成接口
interface AnimalYell{
    yell():void
}


//定义动物类
class Animal {
}


// 定义Mammal类,继承Animal类,实现AnimalYell接口

class Mammal extends Animal implements AnimalYell {
    yell() {
        console.log('动物发出叫声');
    }
}

//Dog,Cat类继承父类Mammal,重写yell方法
// 这里也可以直接让Dog,Cat实现AnimalYell接口
//之所以继承,是更大程度的复用父类Mammal的特点(属性和方法)

class Dog extends Mammal {
    yell(){
         console.log('汪汪...');
    }
}

class Cat extends Mammal {
    yell(){
         console.log('喵喵...');
    }
}



// 一个类可以实现多个接口
interface AnimalWalk{
    walk():void
}

//Snake实现AnimalWalk,AnimalYell两个接口
//实现接口要实现每个接口中的方法
//若以下代码缺少yell或walk的实现就会报错
class Snake implements AnimalWalk,AnimalYell{
    yell(){
        console.log("嘶嘶...")
    }

    walk(){
        console.log("爬行...")
    }
}

new Mammal().yell()//动物发出叫声
new Dog().yell()//汪汪...
new Cat().yell()//喵喵...
new Snake().yell()//嘶嘶...
new Snake().walk()//爬行...

2. 接口继承接口

类可以继承,接口也可以。实际上,ts中接口还能继承类,这在其他面向对象语言中并不允许(如java)。


//将手机功能抽象为一个接口
interface BasePhoneFunction {
  call(): string; //打电话
  send(message: string): string[]; //群发短信
}

// 在基础功能上,还有其他功能
interface PhoneFunction extends BasePhoneFunction {
  game(): void; //打游戏
  listen(): void; //听音学
  watchTV(...tvName:string[]): void; //看电视
  note(): string[]; //便签
}

class Phone implements PhoneFunction {
  call() {
    return "hi, tom,when are we going to coding?";
  }

  send(message: string) {
    console.log(message);
    return ["tom", "jack", "july"];
  }

  game(){
    console.log("打游戏")
  }
  listen(){
    console.log("听音乐")
  }
  //注意这里的rest参数用法,形参名不必和接口定义的形参名一致 tvName
  watchTV(...args:string[]){
    console.log(`看${args.toString()} `)
  }

  note(...args:string[]){
    return [...args]
  }

}


const p=new Phone()
console.log(p.call())//hi, tom,when are we going to coding?
p.send("新年快乐") //新年快乐
console.log(p.send("新年快乐"))// 新年快乐 ["tom", "jack", "july"]
p.game()//打游戏
p.listen()//听音乐
p.watchTV("琅琊榜","将夜")//看琅琊榜,将夜
console.log(p.note("吃饭","睡觉","打豆豆"))//[ '吃饭', '睡觉', '打豆豆' ]

3. 接口继承类

这看起来有些不可思议,接口怎么能继承一个类呢?ts毕竟是ts有它的特殊性,就像js对象不支持反向映射,但是ts的枚举本质就是对象,就能支持反向映射(基于数字的枚举)

//注意,这种写法不能缺少构造函数
class Person {
  eat: string;
  drink: string;
  constructor(eat:string,drink:string){
    this.eat=eat
    this.drink=drink
  }
}

//接口PersonAction 继承类Person , 代码正常运行
//接口PersonAction能有什么用呢?定义对象啊
interface PersonAction extends Person {
  play: string;
}

const p: PersonAction={
  eat:"酸菜鱼",
  drink:"可乐",
  play:"coding"
}

3.1 接口继承类的本质

实际上,接口能继承类是因为ts中类具有特殊性,既是用于实例化操作的类,又是某种类型规范


class Person {
  eat: string;
  drink: string;
  constructor(eat:string,drink:string){
    this.eat=eat
    this.drink=drink
  }
}

//这是最常规的用法了
new Person("酸菜鱼","可乐")

//还可以将其作为一种类型,定义对象
const p:Person={
    eat:"酸菜鱼",
    drink:"可乐"
}


之前定义对象都是用的接口,现在可以用类,这说明ts中的类和接口存在共性。回到最初,接口继承类,来探寻答案

class Person {
  eat: string;
  drink: string;
  constructor(eat:string,drink:string){
    this.eat=eat
    this.drink=drink
  }
}

//其实这种写法等价于下边这种写法
interface PersonAction extends Person {
  play: string;
}


//=>step1, 注意看BasePerson和Person的异同
interface BasePerson {
     eat: string;
     drink: string;
}
//=>step2
interface PersonAction extends BasePerson {
  play: string;
}

//这样依旧ok,Person类实际被拆解成了接口
 const p:PersonAction={
  eat:"酸菜鱼",
  drink:"可乐",
  play:"coding"
}


3.2 同与不同

经过上述探索,得出结果:ts接口继承类本质是接口继承接口。但是被转换成接口的类(BasePerson),和原始的类(Person),差异在哪?再看一个例子。


 class Person {
  //实例属性
  eat: string;
  drink: string;
  constructor(eat:string,drink:string){
    this.eat=eat
    this.drink=drink
  }
  //实例方法
  log():void{
      console.log("log...")
  }

  //静态属性
  static age:number=18;
  //静态方法
  static log():void{
    console.log("static log...")
  }
}

console.log(Person.age)//18
Person.log()//static log...


// 该类被接口继承的时候 发生的变化

interface BasePerson {
  eat: string
  drink: string
  log():void
}

  • 没有构造函数。 这其实很正常,定义类型规范而已,又不是实例化,要构造函数干嘛?
  • 只有实例属性和实例方法会被保留, 静态属性或静态方法不被继承

访问修饰符

java中访问修饰符4种(public/protected/default/private),ts中的访问修饰符3种(public/protected/private)

1. public

public是权限最高的,默认就是public,可自由的访问程序里定义的成员

// 默认写法
class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

// 等价于这种写法
class Person {
 public name: string;
 public constructor(name: string) {
    this.name = name;
  }
}

console.log(new Person("tom").name)//tom

2. private

class Person {
 private name: string;
 private constructor(name: string) {
    this.name = name;
  }
}

console.log(new Person("tom").name)//报错,外部访问不到

3. protected

class Person {
  protected name: string;
  protected constructor(name: string) {
    this.name = name;
  }
}

class Student extends Person {
  constructor(name: string) {
    super(name); //调用父类构造函数
  }

  getName(): string {
    //Student是Person的子类,所以可以拿到protected修饰的name属性
    return this.name;
  }
}

console.log(new Student("tom").getName()); //tom

声明合并

如果定义了两个相同名字的函数(这有些像重载),枚举,接口,或类,那么它们会合并成一个类型

1. js中的同名函数

//如果是js,同名函数后者会覆盖前者

function f1(){
    console.log("f1 top")
}
function f1(){
 console.log("f1 bottom")
}

f1();//f1 bottom

2.1 基于参数个数的函数重载

ts支持函数重载(参数个数/参数类型/参数顺序/返回值,任意一个不同都可形成重载),值得注意的是,需要先声明,后实现


//对于重载的声明
function computed(m: number): number;
function computed(m: number,n:number): number;

//这里是具体实现
function computed(m: number ,n?: number): number {

    if(n){
        return m+n
    }else{
        return 2*m
    }

}
console.log(computed(2))//4
console.log(computed(1,2))//3

2.2 基于参数类型的函数重载

对于不同类型的参数,在实现时候使用管道符|进行分隔

function computed(m: number,n:string): number;
function computed(m: number,n:number): number;

function computed(m: number ,n: number|string): number {

    if(typeof(n)==="string"){
        return +n+m
    }else{
        return m+n
    }

}

console.log(computed(1,2))//3
console.log(computed(1,"2"))//3

2.3 基于参数顺序的函数重载

m,n参数类型的顺序不一致,也可形成重载

function computed(m: number,n:string): number;
function computed(m: string,n:number): number;

function computed(m: number|string ,n: number|string): number {

    if(typeof(m)==="string"){
        m=+m
    }
    if(typeof(n)==="string"){
        n=+n
    }
    return m+n

}

console.log(computed("1",2))//3
console.log(computed(1,"2"))//3

2.4 基于返回值的函数重载

function computed(m: number): number;
function computed(m: number): string;

function computed(m: number): number|string {
    return Math.random()>0.5?m:m.toString()
}

console.log(typeof computed(1))// number or string

3 枚举的声明合并

定义多个同名的枚举类型也会发生声明合并,但是要注意,只有第一个声明的枚举可以省略赋值,后边的不行


enum Language{
    java,
    php,
    node,
}

//这里需要手动赋值
enum Language{
    python=4
} 

//编译后是这样的

"use strict";
var Language;
(function (Language) {
    Language[Language["java"] = 0] = "java";
    Language[Language["php"] = 1] = "php";
    Language[Language["node"] = 2] = "node";
})(Language || (Language = {}));
(function (Language) {
    Language[Language["python"] = 4] = "python";
})(Language || (Language = {}));
//# sourceMappingURL=index.js.map


4 接口的声明合并

interface Box{
    width:string
}

interface Box{
    height:string
}

//等价于
interface Box{
    width:string
    height:string
}


4.1 属性类型不同无法合并

//这两个无法合并,height类型不一致
interface Box{
    height:string
}

interface Box{
    color:string
    height:number
}

4.2 接口中方法的合并,与函数的合并一样

interface Alarm {
    price: number;
    alert(s: string): string;
}
interface Alarm {
    weight: number;
    alert(s: string, n: number): string;
}

//上述写法等价于

interface Alarm {
    price: number;
    weight: number;
    alert(s: string): string;
    alert(s: string, n: number): string;
}
 

5 类的合并与接口的合并规则一致。

补充章节

1. never类型

never类型表示的是那些永不存在的值的类型,常在抛出错误时使用。特殊情况下,变量也可以使用never声明,比如一个永远为空的数组。never是任何类型的子类型,也可以赋值给任何类型,但是反过来不可以。(自身除外)


function error(message: string): never {
    throw new Error(message);
}
const emptyArr: never[] = []

2. 常量枚举

与普通枚举相比,常量枚举多了一些限制:常量枚举会在编译阶段被删除,不可包含计算成员

const enum Language {
    java,
    node,
    php
}

 console.log([Language.java, Language.node, Language.php])

 // 非常量枚举编译后是这样的
 "use strict";
var Language;
(function (Language) {
    Language[Language["java"] = 0] = "java";
    Language[Language["node"] = 1] = "node";
    Language[Language["php"] = 2] = "php";
})(Language || (Language = {}));
console.log([Language.java, Language.node, Language.php]);
//# sourceMappingURL=index.js.map


// 常量枚举编译后是这样的
 "use strict";
console.log([0 /* java */, 1 /* node */, 2 /* php */]);
// 如果tsconfig.json中removeComments属性为true,就是下面这样,删除注释
console.log([0, 1, 2]);
//# sourceMappingURL=index.js.map


// 不可含有计算属性
// 这样会报错
const enum Color {Red, Yellow, Blue = "blue".length};
console.log(Color.Blue);

3 函数类型接口

接口可用于规范对象,而函数也是对象,所以接口也可以用于规范函数,但有些差别


//作为对象方法使用的普通函数
interface User{
    name:string,
    say:(msg:string)=>string
}


//对方法的入参和返回值进行约束的函数类型接口
interface Say{
    (msg:string):string
}

// 所以上边的写法还可以这样

interface User{
    name:string,
    say:Say
}

const u:User={
    name:"tom",
    say:(msg:string):string=>{
        return msg
    }
}

console.log(u.say("hello"))//hello

泛型

泛型是指在定义函数/接口/类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性,这在面向对象语言中很常见。

1. 初识

从概念出发,泛型可以抽象为一个表示某种特定类型的占位符。


//现在有这样一个需求,log函数传入一个数字作为参数,原样返回,代码如下:

function log1(m:number):number{
    return m
}

//现在需求变了,传入的是个字符串,原样返回,代码如下:
function log2(str:string):string{
    return str
}
// 很明显,log1和log2存在高冗余的代码
// 在不削弱ts静态类型检查能力的情况下,泛型是比any更好的选择
//不一定非要用T表示,这只是个占位符,换成ABCD都行
function log<T>(param:T):T{
    return param
}

//注意看这里:log<T>,函数调用的时候我们并没有手动指定类型,但是不会报错
//这是因为ts的类型推论(合理使用该特性会让代码更简洁)
console.log(log("string"))//string
console.log(log(1024))//1024
console.log(log(true))//true

//也可以是这样的

console.log(log<string>("string"))//string
console.log(log<number>(1024))//1024
console.log(log<boolean>(true))//true



2. 感知

泛型可以使用多个,且具有关联性


//定义一个交换函数swap,入参为一个元组,返回值为交换后的元组

function swap<T, U>(tuple: [T, U]): [U, T] {
    return [tuple[1], tuple[0]];
}

console.log(swap(['lengyuexin', '冷月心'])); // ['冷月心', 'lengyuexin']

//关联性指的是所有用同一个占位符(泛型)占位的,在类型确定时都会被统一替换成该类型
//当然,这可能看起来有点像废话...
//手动指定类型 <string,string>,第一个string替换所有的T,第二个string替换所有的U
console.log(swap<string,string>(['lengyuexin', '冷月心'])); // ['冷月心', 'lengyuexin']

3. 不惑

泛型还可以只用某一部分,或者称其为泛型变量


//现在有这样一个需求,输出某个数组的长度
//这样会报错,T身上没有length属性
function getLength<T>(arr:T):number{
    return arr.length
}

//可以更精细一些,既然确定是数组,就将确定部分具体化
//Array<T>,这里泛型就不是完全的占位了,是部分占位
function getLength<T>(arr:Array<T>):number{
    return arr.length
}

// 还可以更稳妥的加入断言,以下两种断言方式都ok

function getLength<T>(arr:Array<T>):number{
    return (<Array<T>>arr).length
}

function getLength<T>(arr:Array<T>):number{
    return (arr as Array<T>).length
}


4 洞玄

补充章节提到了函数类型接口,实际上,在接口中也可以接入泛型


//不接入泛型的函数接口
interface Say{
    (msg:string):string
}

//接入泛型的函数接口

interface Say<T>{
    (msg:T):T
}


//声明函数-此时需要手动指定类型
const say:Say<string>=(msg:string)=>msg

console.log(say("函数接口中的泛型..."))//函数接口中的泛型...


5 知命

泛型也可以用在类中,作用于类的成员属性和方法。

class Print<T> {
  private store: Array<T> = [];

  inner(msg: T) {
    this.store.push(msg);
  }

  outer() {
    console.log(this.store);
  }
}

//使用类型推论可以不限制类型的插入 const p = new Print();
//一旦限定,就只能同类型的插入
const p = new Print<string>();

p.inner("吃饭");
p.inner("睡觉");
p.inner("打豆豆");
// p.inner(1024);
p.outer()//['吃饭','睡觉','打豆豆']


可选链

可选链不是ts专属,自ts3.7版本后内置,它确实解决了对象属性或方法访问前判断是否存在的痛点,可选链更直观

来自mdn的解释

可选链操作符?.能够去读取一个被连接对象的深层次的属性的值而无需明确校验链条上每一个引用的有效性。?.运算符功能类似于.运算符,不同之处在于如果链条上的一个引用是null 或 undefined,.操作符会引起一个错误,?.操作符取而代之的是会按照短路计算的方式返回一个undefined。当?.操作符用于函数调用时,如果该函数不存在也将会返回undefined。 当访问链条上可能存在的属性却不存在时,?.操作符将会使表达式更短和更简单。当不能保证哪些属性是必需的时,?.操作符对于探索一个对象的内容是很有帮助的。

interface Person {
  name: string;
  age: number;
  say: (str: string) => string;
}

const p: Person = {
  name: "tom",
  age: 18,
  say(str: string) {
    return str;
  }
};

p?.name;
p?.age;
p?.say("hello girl")
//p?.log() //报错 log方法不存在

//看下编译后结果吧

"use strict";
var p = {
    name: "tom",
    age: 18,
    say: function (str) {
        return str;
    }
};
p === null || p === void 0 ? void 0 : p.name;
p === null || p === void 0 ? void 0 : p.age;
p === null || p === void 0 ? void 0 : p.say("hello girl");
//# sourceMappingURL=index.js.map

类型兼容性

ts的类型兼容是基于结构化类型的,简单来说:如果x要兼容y,那么y至少具有与x相同的属性。换言之,具有公共属性,就具备了类型兼容的条件。

1. 对象的类型兼容性


//来看个有意思的例子
interface Animal{
  name:string
}

interface Person{
  name:string
}

let a:Animal={
  name:"panfish"
};
let p:Person={
  name:"tom"
}

//注意这里,两种赋值都不报错
//ts的结构化类型,某种程度上panfish和tom等价
p=a;
a=p;

// 接下来在上边的例子中做一下改动,Person加一个age属性

interface Person{
  name:string
  age:number
}

//这里也加一条属性
let p:Person={
  name:"tom",
  age:18
}

//其他不变
p=a;//报错,结构化类型角度考虑,a中没有age属性,可多不可少
a=p;//不报错


2. 函数的类型兼容性

函数也是对象,同样适用类型兼容性规则,但只关注参数类型和顺序而不关注参数名

let fn1 = (m: number): number => m;
let fn2 = (n: number, s: string): number => +s+n;

//这里好理解
console.log(fn1(100));//100
console.log(fn2(100,"1"));//101

// 赋值--注意:返回值不一样肯定是不兼容的

fn2=fn1;//ok
fn1=fn2;//报错

// 如果调换f2参数顺序为 s: string,n: number,以下皆报错
fn2=fn1;//报错
fn1=fn2;//报错


3. 枚举的类型兼容性

枚举类型与数字类型双向兼容,基于字符串的枚举不兼容,跨类型的枚举类型之间不兼容。

3.1 基于数字的枚举


enum Time {
  moring,
  noon,
  night
}

let moring = Time.moring;
let num:number = 10;

//这里很容易理解,因为 Time.moring拿到的值就是一个数字类型
moring=num;//ok
num=moring//ok

3.2 跨类型枚举


enum Language{
  java,
  js,
  node,
  php,
  python
}

//跨类型的枚举类型之间不兼容
let js=Language.js;
js=moring;//error
moring=js;//error

4 类的类型兼容性

类的类型兼容性大体和对象那部分的相似,但要注意的是:类有静态部分和实例部分的类型。静态属性和静态方法以及构造函数会在结构化类型兼容过程中被忽略,只比较实例属性和实例方法。

public 级别的类型兼容性


//实际比较的是实例属性name,实例方法say
class Animal {
  //实例属性
  name: string;
  //构造函数
  constructor(name: string) {
    this.name = name;
  }
  //实例方法
  say():void{
    
  }
  //静态方法
  static say():void{
     console.log("静态方法 say")
  }

  //静态属性
  static flag: string = "animal";
}

//实例属性name,age 实例方法say
class Person {
  //实例属性
  name: string;
  age:number;
  constructor(name: string,age:number) {
    this.name = name;
    this.age=age;
  }

  say():void{
   
  }
}

let a: Animal = {
  name: "cat",
  say:()=>{
    console.log("say animal")
  }
};
let p: Person = {
  name: "tom",
  age:18,
  say:()=>{
    console.log("say person")
  }
};

a.say()//say animal
p.say();//say person

 a = p;//ok
 p = a;//error a比p少一个age属性,无法兼容


private/protected 级别的类型兼容性

不同于public级别的类型兼容性,private/protected 要求更严格,必须来自同一个类


//protected 和 private类似   

class Cat{
  //即使一方属性私有化,另一方是public也不行
  private name:string;
  constructor(name:string){
    this.name=name;
  }
}

class Animal{
   name:string;
   constructor(name:string){
    this.name=name;
  }
}

let c:Cat={
  name:"cat"
};
let a:Animal={
  name:"animal"
}
 c=a;//error
 a=c;//error

总结

这篇文章展现的只是ts世界的冰山一角,其全貌远超于此(坑更多),希望这篇文章对你有所帮助。能力有限,如有错误,请掘友们不吝赐教。温馨提示:注意ts版本号和tsconfig.json的配置

参考链接

  • 掘金ts小册:https://juejin.cn/book/6844733813021491207
  • ts中文手册:https://zhongsp.gitbooks.io/typescript-handbook
  • ts入门教程:https://ts.xcatliu.com
  • ts常见问题整理:https://juejin.cn/post/6844904055039344654
  • mdn可选链:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/%E5%8F%AF%E9%80%89%E9%93%BE