likes
comments
collection
share

Typescript基础篇

作者站长头像
站长
· 阅读数 84

TypeScript概念

TypeScript是JS的超集,ts是没办法直接在node或浏览器直接运行的,需要编译成普通JS运行

Typescript官网

优点

  • 更好的错误提示,写代码的时候就能发现错误
  • 语法提示更好
  • 代码语义化、可读性更好

概念

  • 类型注解:我们告诉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: 1second的类型为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:类型别名

相同

  1. 都可以描述对象或者函数
  2. 都允许拓展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

不同点

  1. type 可以声明基本类型别名,联合类型,元组等类型
  2. interface能够声明合并(同名interface会自动合并)
  3. 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

参考:juejin.cn/post/684490…

交叉类型

在 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, // 允许编译器编译JSJSX文件
    "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// 打印编译的文件(包括引用的声明文件)
  }
}

注意

  1. src下有index.ts和index.d.ts声明文件,index.d.ts可能不会被编译

官网:需要注意编译器不会去引入那些可能作为输出的文件,假设我们包含了index.ts,那么index.d.ts和index.js会被排除在外,通常来讲,不推荐只有扩展名的不同来区分目录下的文件(由此可见以上三个文件名都是index,只是扩展名不一样而已,导致只编译了ts扩展名文件)

可用工具

转载自:https://juejin.cn/post/7166168447078891550
评论
请登录