Typescript基础篇
TypeScript概念
TypeScript是JS的超集,ts是没办法直接在node或浏览器直接运行的,需要编译成普通JS运行
优点
- 更好的错误提示,写代码的时候就能发现错误
- 语法提示更好
- 代码语义化、可读性更好
概念
- 类型注解:我们告诉TS变量类型
function getTotal(first: number, second: number) {
    return first + second
}
const total = getTotal(1, 2)
// 函数的参数必须手动注解类型,因为它可以是任何类型,ts无法推断出来,但是total就不用了,因为两个number类型的值加起来一定是数值,ts会推断出total是number类型
- 类型推断:TS自动尝试分析变量的类型,那我们不用多余去加变量类型
const first = 1
const second = 2
const total = first + second
 
 // 这三个都可以不用加类型,ts自动推断的出来,会发现first的类型是first: 1,second的类型为2,total为number
指令
- tsc --init初始化typescript项目,生成tsconfig.json配置文件
- tsc file.ts编译ts文件成js
基础类型
// 基础类型
const count: number = 123
// 对象类型
const teacher: {
    name: string,
    age: number
} = {
    name: 'chenjiaobin',
    age: 18
}
class Person {}
const jiaobin: Person = new Person()
// 函数类型
const add: () => string = () => {
    return 'chenjiaobin'
}
函数类型
// 虽然a+b能推断出number,但是避免函数写错a+b+'',导致返回字符串,所以最好把返回值的类型也注解下
function add(a: number, b:number): number {
    return a + b
}
// 另外一种注释方式
const add: (str: string) => number = (str) => {
    return parseInt(str, 10)
}
// 函数不返回值,如果函数return会提示错误
function add(a: number, b:number): void {
   console.log(a, b)
}
// 解构注解,不能写成{ a: number, b: number }
function add({ a, b } = { a: number, b: number }):number {}
数组类型
简单数组类型注释
// 数组每一项必须是number
const arr: number[] = [1,3,4]
// 每项可以是number|string
const arr: (number | string) = [1, '2']
数组存对象的注释
// 定义类型别名
type User = { name: string; age: number }
const objarr: User[] = [
    {
        name: 'chenjiaobin',
        age: 18
    }
]
元祖tuple:当数组数量固定,且需要确定数组每一项的类型需要用到元祖
// 每个位置的类型是确定的
const info: [string, number, string] = ['a', 12, 'b']
const info: [string, number, string][] = [
    ['chen', 12, 'male'],
    ['jiaobin', 12, 'male']
]
interface VS type
- interface:接口
- type:类型别名
相同
- 都可以描述对象或者函数
- 都允许拓展extends
interface User {
    name: string
}
type Person = {
    name: string
}
interface SetUser {
  (name: string, age: number): void;
}
type SetUser = (name: string, age: number): void;
// 拓展语法不一样,但可以互相继承
interface User2 extends User {}
interface User2 extends Person {}
type Person2 = Person & { age: number }
type Person2 = Person & User
不同点
- type 可以声明基本类型别名,联合类型,元组等类型
- interface能够声明合并(同名interface会自动合并)
- interface支持特殊属性定义(只读、可选项)
interface Shape {
    width?: number; // 可有可无的属性
    readonly color: string; // 只读,添加该属性注释的变量不能修改color这个值
    [propName: string]: any; // 这个使得变量可以添加除了width和color的其它属性
    say(): string // 定义方法
}
implements
一个class类通过implements使用接口注释
class User implements Info {
    name = 'chenjiaobin';
    age = 18;
    say() {
        return 'hello'
    }
}
class类使用
- public:变量和方法在类内部和外部都能被使用
- private:变量和方法只能在类内部被使用,且不能在继承子类内使用
- protect:变量和方法能在类内部和继承子类内使用,但不能在外部使用
super什么时候使用?
- 当父类的方法被继承类重写覆盖的话,还想要调用父类方法的时候,可以通过super.methodname()调用
- 继承子类必须在constructor中执行super(),手动调用父类的构造器
class Parent {
    constructor(public name: string) {
        
    }
}
class Child extends Parent {
    construcor(public age: number) {
        // 必须手动调用父类construcor,就算父类没设置constrctor也要执行super()
        super('jiaobin')
    }
}
const childPerson = new Child(18)
getter和setter 作用:可以保护类中的私有属性
class Person {
  constructor(private _name: string) { }
  get name() {
    return this._name + ' chen'
  }
  set name(name: string) {
    const realname = name.split(' ')[0]
    this._name = name
  }
}
const person = new Person('jiaobin')
console.log(person.name) // jiaobin chen  (name不需要在后面加括号)
person.name = 'jiaobin chen' // 实体中的_name实际存储的还是jiaobin
console.log(person.name) // jiaobin chen
class实现单体模式和static的使用 static的方法或属性是设置在类上,而不是在实体上
class Person {
  // 存储实例
  private static instance: Person
  private constructor(private _name: string) { }
  static GetInstance() {
    if (!this.instance) {
      this.instance = new Person('jiaobin')
    }
    return this.instance
  }
}
// const person1 = new Person('jiaobin')
// const person2 = new Person('Bob') // 实例不了,因为construcor设置了private,只能在类内部实例
const person1 = Person.GetInstance() //getInstance设置了static,所以可以直接从类中获取
const person2 = Person.GetInstance() // person1和person2是同个实例
小技巧
class Person {
    // 传统写法
    public name: string,
    constructor(name: string) {
        this.name = name
    }
    
    // 简化写法
    constructor(public name: string) {
        
    }
}
const person = new Person('jiaobin')
console.log(person.name) // jiaobin
类型保护
联合类型(a | b)中,ts只能推断出a和b的公共属性,各自特有的属性调用会报错,解决如下:
- 类型断言
interface Dog {
    fly: Boolean;
    sing: () => {};
}
interface Bird {
    fly: Boolean;
    Bark: () => {}
}
function animal(animal: Dog | Bird) {
    // animal.sing() // 这样是会报错的,因为Bird里面没有sing方法
    // 解决方法(类型断言)
    if (animal.fly) {
        (animal as Bird).bark() // 强制告诉ts是Bird
    } else {
        (animal as Dog).sing()
    }
}
- in 语法做类型保护
function animal(animal: Dog | Bird) {
    if ('sing' in animal) {
        animal.sing()
    } else {
        
    }
}
- typeof 语法做类型保护
function animal(first: number | string, second: number | string) {
    if (typeof first === 'string' || typeof ....) {
        return `${first}${second}`
    }
    return first + second
}
- instanceof语法做类型保护
// 这里必须用class定义,interface没有instanceof
class numberObje {
    count: number
}
function show(obj: Object | numberObje) {
    if (obj instanceof numberOjb) {}
}
枚举
默认值是0开始
enum Status {
    ONLINE,
    OFFLINE,
    DELETED
}
console.log(Status.ONLINE,Status.OFFLINE,Status.DELETED) // 0,1,2
可以设置值
enum Status {
    ONLINE,
    OFFLINE = 3,
    DELETED
}
console.log(Status.ONLINE,Status.OFFLINE,Status.DELETED) // 0,3,4
支持反向映射查询
enum Status {
    ONLINE = 2,
    OFFLINE,
    DELETED
}
console.log(Status.ONLINE,Status.OFFLINE,Status.DELETED) // 2,3,4
console.log(Status[2]) // ONLINE
泛型
// 需求:first和second必须同时是string或同时是number,以下是做不到的
function join(first: string | number, second: string | number) {
    return `${first}${second}`
}
泛型实现以上需求
function join<T>(first: T, second: T) {
    
}
join<number>(1,2) // 两个参数必须是number才不会报错
// 返回值也设置成类型必须跟两个参数一样
function join<T>(first: T, second: T): T {
    
}
// 也可以当成数组里面的类型
function join<T>(first: Array<T>) {
    
}
join<number>([1])
类中的泛型
T extends Item继承使用
interface Item {
    age: string
}
// T继承Item,表明该泛型中的每一项需要包括Item的类型声明
class People<T extends Item> {
    constructor(private info: T[]) {},
    getInfo(key: number): T {
        return this.info[index]
    }
}
const result = new People([{age: 18}])
避免T可以指定任何类型,通过T extends number | string指定T只能是number或string
class People<T extends number | string> {
    constructor(private info: T[]) {},
    getInfo(key: number): T {
        return this.info[index]
    }
}
onst result = new People<number>([1])
泛型中keyof的使用
interface Person {
    name: string,
    age: number,
    hobby: string
}
class Student {
    constructor (private userInfo: Person)
    // 错误写法
    getUserInfo (key: string) {
        // 这里会报错,不严谨,因为key可能不存在Person中
        return this.userInfo[key]
    }
    // 正确写法
    getUserInfo<T extends keyof Person> (key: T): Person[T] {
        return this.info[key]
    }
}
const info = new Student({
    name: 'chenjiaobin',
    age: 18,
    hobby: 'basketball'
})
info.getUserInfo(name)
T extends keyof Person 会遍历Person接口定义
type T = 'name'
type T = 'age'
type T = 'hobby'
ts是可以把变量定义成具体值类型的
type name = 'chenjiaobin'
const myName: name = 'chen' // 报错
const myName: name = 'chenjiaobin' // 正确,只能设置这个值
namespace-命名空间
类似模块化开发的方式,尽少的暴露全局变量
namespace Page {
    class Header {}
    class Footer {}
    export class GeneratePage {
        new Header()
        new Footer()
    }
}
// 这样写暴露的全局变量是Page,可以执行new Page.GeneratePage(),而Page.Header是获取不到的
declare-声明变量
解决ts识别不了一些全局变量的问题,通过创建name.d.ts(d.ts文件属于类型描述文件)让ts去识别语法
// 需求:ts中使用jquery的语法,需要声明$全局变量,要不在ts文件中使用$会报错
// index.ts
$(function () {
    console.log('测试声明变量')
    $('div').html('内容') // 需要定义函数且参数是字符串类型的声明
    new $.fn.init() // 需要定义对象类型声明
})
// jquery.d.ts
interface JqueryInstance {
    html: (html: string) => JqueryInstance
}
// 定义全局函数,传参也是函数
declare function $(loadFunc: () => void): void
declare function $(selector: string): JqueryInstance
// 对象类型定义,以及命名空间嵌套
declare namespace $ {
    namespace fn {
        class init {}
    }
}
Tip:能同时在一个文件定义同名函数,这叫函数重载
以上jquery的引入和声明也可以使用ES6模块化实现
// index.ts
import $ from 'jquery' // 这个时候jquery会报没有声明的错
$(function () {
    console.log('测试声明变量')
    $('div').html('内容') // 需要定义函数且参数是字符串类型的声明
    new $.fn.init() // 需要定义对象类型声明
})
// jquery.d.ts
// 声明jquery模块
declare module 'jquery' {
    interface JqueryInstance {
        html: (html: string) => JqueryInstance
    }
    function $(loadFunc: () => void): void  // 这里的declare可以去掉了
    function $(selector: string): JqueryInstance
    // 对象类型定义,以及命名空间嵌套
    namespace $ {
        namespace fn {
            class init {}
        }
    }
    export = $  // 要导出$
}
TS 引入第三库存在问题
- 问题一:第三方库.d.ts文件类型描述不准确
- 问题二:对第三库做修改的时候,实际上类型并不能修改
问题一解决
// 问题:express中post请求中获取req.body中的值,类型是any
const { name } = req.body // 此时的name是any类型,是不太符合ts的
// 打开@types/express中的index.d.ts发现body的类型定义是any,那么我们可以通过定义接口并继承Request就可以定义body里面包含的属性了
import { Request } from 'express'
interface RequestBody extends Request {
    body: {
        [propName: string]: string | underfind
    }
}
router.post('/setName', (req: RequestBody, res: Response) => {
    // 此时name的类型会被推断为string|underfind
    const { name } = req.body
})
问题二解决
// 问题:给express中的request中添加一个字段,而实际express类型声明文件不包含添加的字段,会报错
import express, { Request, Response, NextFunction } from 'express'
const app = exress()
app.use((req: Request, res: Response, next: NextFunction) => {
    req.age = 18 // 此时会报错,因为req定义不包含age
    next()
})
// 解决:可以通过创建.d.ts文件和express的.d.ts文件糅合,解决此问题
express.d.ts
// 命名空间跟Express的.d.ts一样
declare namespace Express {
    interface Request {
        age: number
    }
}
装饰器
对类、函数、属性进行额外的修饰,使用时需要在tsconfig.json中开启"experimentalDecorators": true ,装饰器的触发时机是在Javascript的预编译阶段,而非Typescript的编译阶段。
装饰器类型有:类声明装饰器、方法装饰器、属性装饰器、访问器装饰器、参数装饰器
装饰器声明
// 类装饰器
declare type ClassDecorator = <T extends new (...arg: any[]) => any>(
  constructor: T
) => T;
// 属性装饰器 | 访问器装饰器
declare type PropertyDecorator = (
  target: Object,
  propertyKey: string | symbol
) => void;
// 方法装饰器
declare type MethodDecorator = <T>(
  target: Object,
  propertyKey: string | symbol,
  descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;
// 参数装饰器
declare type ParameterDecorator = (
  target: Object,
  propertyKey: string | symbol,
  parameterIndex: number
) => void;
类装饰器
- 装饰器本身是个函数
- 类装饰器接受的参数是构造函数
- 装饰器通过@符号来使用
// 装饰器
function testDecorator<T extends new (...arg: any[]) => any>(constructor: T) {
  return class extends constructor {
    age = 20
    getAge() {
        return this.age
    }
  }
}
@testDecorator
class Test {
  age: number;
  constructor(age: number) {
    this.age = age
  }
}
const testInstance = new Test(18)
console.log(testInstance) // { age: 20 }
注:先执行Test再执行装饰器
以上存在问题,testInstance调用getAge的时候联想不出来,因为Test没有定义此方法,只是装饰器有而已,解决如下:
function testDecorator() {
  return function<T extends new (...arg: any[]) => any>(constructor: T) {
    return class extends constructor {
      age = 20
      getAge() {
        return this.age
      }
    }
  }
}
const Test = testDecorator()(
  class Test {
    age: number;
    constructor(age: number) {
      this.age = age
    }
  }
)
const testInstance = new Test(18)
方法装饰器
- target 普通方法是类的原型prototype;静态方法是类的构造函数(类的定义)
- propertyKey 方法的名称( string | symbol )
- descriptor : 一个TypedPropertyDescriptor,如果您不熟悉描述符的键,请看Object.defineProperty
function getNameDecorator(target: any, key: string, descriptor: TypedPropertyDescriptor<any>) {
  console.log(target) // Test2 { getName: [function] }
  const originalMethod = descriptor.value; // save a reference to the original method
  descriptor.value = function(...args: any[]) {
      console.log("The method args are: " + JSON.stringify(args));
      const result = originalMethod.apply(this, args);
      console.log("The return value is: " + result);
      return result;
  };
  return descriptor;
}
function getAge(target: any, key: string, descriptor: TypedPropertyDescriptor<any>) {
    console.log(target) // [Function: Test2] { getAge: [Function] }
}
class Test2 {
  name: string
  constructor(name: string) {
    this.name = name
  }
  @getNameDecorator
  getName(name: string) {
    return this.name
  }
  @getAge
  getAge(age: number) {
      reurn this.age
  }
}
const Test2Instance = new Test2('jiaobin')
console.log(Test2Instance.getName('jiaobinchen'))
// 输出
The method args are: ["jiaobinchen"]
The return value is: jiaobin
jiaobin
属性装饰器
参数和方法装饰器一样,少了第三个参数而已
function nameDecorator(target: any, key: string): any {
    // 可以自己重写第三个参数,并且要return出去
  const descriptor: TypedPropertyDescriptor<any> = {
    writable: false
  }
  return descriptor
}
class Test2 {
  @nameDecorator
  name = 'jiaobin'
}
const Test2Instance = new Test2()
console.log(Test2Instance.name)  
参数装饰器
- 参数一:同上
- 参数二:同上
- 参数三:参数所处位置索引
function myDecorator(target: any, key: string, paramsIndex: number): any {
  console.log(target, key, paramsIndex)
}
class MyClass {
  myMethod(@myDecorator myParameter: string) {}
}
// { myMethod: [Function] } myMethod 0
装饰器执行顺序
同一位置有多个装饰器的执行顺序:自顶向下调用每个装饰工厂方法,再自内向外调用装饰器方法
@clazz()
class CallSequence {
  @property() property: undefined;
  @method()
  mtehod(@parameter1() p: any, @parameter2() p2: any) {}
  private _a = 1;
  @accessor()
  get a() {
    return this._a;
  }
}
function clazz() {
  console.log("ClassDecorator before");
  return (target: any) => {
    console.log("ClassDecorator after");
  };
}
function method() {
  console.log("MethodDecorator before");
  return (t: any, k: any, p: any) => {
    console.log("MethodDecorator after");
  };
}
function property() {
  console.log("PropertyDecorator before");
  return (t: any, k: any) => {
    console.log("PropertyDecorator after");
  };
}
function accessor() {
  console.log("AccessorDecorators before");
  return (t: any, k: any) => {
    console.log("AccessorDecorators after");
  };
}
function parameter1() {
  console.log("ParameterDecorator1 before");
  return (t: any, k: any, i: any) => {
    console.log("ParameterDecorator1 after");
  };
}
function parameter2() {
  console.log("ParameterDecorator2 before");
  return (t: any, k: any, i: any) => {
    console.log("ParameterDecorator2 after");
  };
}
打印结果
PropertyDecorator before
PropertyDecorator after
MethodDecorator before
ParameterDecorator1 before
ParameterDecorator2 before
ParameterDecorator2 after
ParameterDecorator1 after
MethodDecorator after
AccessorDecorators before
AccessorDecorators after
ClassDecorator before
ClassDecorator after
交叉类型
在 JavaScript 中, extend 是一种非常常见的模式,在这种模式中,你可以从两个对象中创建一个新对象,新对象拥有着两个对象所有的功能
function extend<T extends object, U extends object>(first: T, second: U): T & U {
  const result = <T & U>{};
  for (let id in first) {
    (<T>result)[id] = first[id];
  }
  for (let id in second) {
    if (!result.hasOwnProperty(id)) {
      (<U>result)[id] = second[id];
    }
  }
  return result;
}
const x = extend({ a: 'hello' }, { b: 42 });
// 现在 x 拥有了 a 属性与 b 属性
const a = x.a;
const b = x.b;
tsconfig.json基本配置
{
  "compilerOptions": {
    "incremental": true, // TS编译器在第一次编译之后会生成一个存储编译信息的文件,第二次编译会在第一次的基础上进行增量编译,可以提高编译的速度
    "tsBuildInfoFile": "./buildFile", // 增量编译文件的存储位置
    "diagnostics": true, // 打印诊断信息 
    "target": "ES5", // 目标语言的版本
    "module": "CommonJS", // 生成代码的模板标准
    "outFile": "./app.js", // 将多个相互依赖的文件生成一个文件,可以用在AMD模块中,即开启时应设置"module": "AMD",
    "lib": ["DOM", "ES2015", "ScriptHost", "ES2019.Array"], // TS需要引用的库,即声明文件,es5 默认引用dom、es5、scripthost,如需要使用es的高级版本特性,通常都需要配置,如es8的数组新特性需要引入"ES2019.Array",
    "allowJS": true, // 允许编译器编译JS,JSX文件
    "checkJs": true, // 允许在JS文件中报错,通常与allowJS一起使用
    "outDir": "./dist", // 指定输出目录
    "rootDir": "./", // 指定输出文件目录(用于输出),用于控制输出目录结构
    "declaration": true, // 生成声明文件,开启后会自动生成声明文件
    "declarationDir": "./file", // 指定生成声明文件存放目录
    "emitDeclarationOnly": true, // 只生成声明文件,而不会生成js文件
    "sourceMap": true, // 生成目标文件的sourceMap文件
    "inlineSourceMap": true, // 生成目标文件的inline SourceMap,inline SourceMap会包含在生成的js文件中
    "declarationMap": true, // 为声明文件生成sourceMap
    "typeRoots": [], // 声明文件目录,默认时node_modules/@types
    "types": [], // 加载的声明文件包
    "removeComments":true, // 删除注释 
    "noEmit": true, // 不输出文件,即编译后不会生成任何js文件
    "noEmitOnError": true, // 发送错误时不输出任何文件
    "noEmitHelpers": true, // 不生成helper函数,减小体积,需要额外安装,常配合importHelpers一起使用
    "importHelpers": true, // 通过tslib引入helper函数,文件必须是模块
    "downlevelIteration": true, // 降级遍历器实现,如果目标源是es3/5,那么遍历器会有降级的实现
    "strict": true, // 开启所有严格的类型检查
    "alwaysStrict": true, // 在代码中注入'use strict'
    "noImplicitAny": true, // 不允许隐式的any类型
    "strictNullChecks": true, // 不允许把null、undefined赋值给其他类型的变量
    "strictFunctionTypes": true, // 不允许函数参数双向协变
    "strictPropertyInitialization": true, // 类的实例属性必须初始化
    "strictBindCallApply": true, // 严格的bind/call/apply检查
    "noImplicitThis": true, // 不允许this有隐式的any类型
    "noUnusedLocals": true, // 检查只声明、未使用的局部变量(只提示不报错)
    "noUnusedParameters": true, // 检查未使用的函数参数(只提示不报错)
    "noFallthroughCasesInSwitch": true, // 防止switch语句贯穿(即如果没有break语句后面不会执行)
    "noImplicitReturns": true, //每个分支都会有返回值
    "esModuleInterop": true, // 允许export=导出,由import from 导入
    "allowUmdGlobalAccess": true, // 允许在模块中全局变量的方式访问umd模块
    "moduleResolution": "node", // 模块解析策略,ts默认用node的解析策略,即相对的方式导入
    "baseUrl": "./", // 解析非相对模块的基地址,默认是当前目录
    "paths": { // 路径映射,相对于baseUrl
      // 如使用jq时不想使用默认版本,而需要手动指定版本,可进行如下配置
      "jquery": ["node_modules/jquery/dist/jquery.min.js"]
    },
    "rootDirs": ["src","out"], // 将多个目录放在一个虚拟目录下,用于运行时,即编译后引入文件的位置可能发生变化,这也设置可以虚拟src和out在同一个目录下,不用再去改变路径也不会报错
    "listEmittedFiles": true, // 打印输出文件
    "listFiles": true// 打印编译的文件(包括引用的声明文件)
  }
}
注意
- src下有index.ts和index.d.ts声明文件,index.d.ts可能不会被编译
官网:需要注意编译器不会去引入那些可能作为输出的文件,假设我们包含了index.ts,那么index.d.ts和index.js会被排除在外,通常来讲,不推荐只有扩展名的不同来区分目录下的文件(由此可见以上三个文件名都是index,只是扩展名不一样而已,导致只编译了ts扩展名文件)
可用工具
转载自:https://juejin.cn/post/7166168447078891550




