Fork me on GitHub

typescript使用入门及react+ts实战

ts介绍

TypeScript是一种由微软开发的自由和开源的编程语言。它是 JavaScript 的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程。

与js关系

image.png

ts与js区别

TypeScript JavaScript
JavaScript 的超集,用于解决大型项目的代码复杂性 一种脚本语言,用于创建动态网页。
强类型,支持静态和动态类型 动态弱类型语言
可以在编译期间发现并纠正错误 只能在运行时发现错误
不允许改变变量的数据类型 变量可以被赋予不同类型的值

使用ts好处?

  • 规范我们的代码,编译阶段发现错误,在原生基础上加了一层定义
  • ts是js的类型化超集,支持所有js,并在此基础上添加静态类型和面向对象思想
  • 静态类型,定义之后就不能改变

ts缺点

  • 不能被浏览器理解,需要被编译成 JS
  • 有一定的学习成本
  • 代码量增加

环境安装

npm install -g typescript 
// yarn方式  
yarn global add typescript

// 检查是否安装成功
tsc -v

// 编译ts文件
tsc index.ts

vscode中监听ts文件自动编译

// 1. 初始化tsconfig.json文件
tsc --init 

2. 左上角菜单点击终端->运行任务->点击typescript->选择tsc监视tsconfig文件

基础类型

boolean、number 和 string 类型

  • boolean
let isDone: boolean = false;
// let isDone: boolean = "123"; //Type 'string' is not assignable to type 'boolean'

赋值与定义的不一致,会报错,静态类型语言的优势就体现出来了,可以帮助我们提前发现代码中的错误。

  • number
let decimal: number = 6;
  • string
let color: string = "blue";

数组类型

let list: number[] = [1, 2, 3];
// let list: Array<number> = [1, 2, 3];

// 数组里的项写错类型会报错
let list: number[] = [1, 2, "3"]; // error

// push 时类型对不上会报错
list.push(4)
list.push("4") // error

如果数组想每一项放入不同类型数据,怎么办?那我们可以使用元组类型

元组类型

元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。

let x: [string, number] = ["hello", 10];

// 写错类型会报错
let x: [string, number] = [10, "hello"];  // Error

// 越界会报错
let x: [string, number] = ["hello", 10, 110];  // Error

// 使用 push 时,不会有越界报错,但是只能 push 定义的 number 或者 string 类型
x.push("hello2")
x.push(11)
console.log(x) //['hello', 10, 'hello2', 11]

// push 一个没有定义的类型,报错
x.push(true) // error

undefined 和 null 类型

let u:undefined = undefined  
let n:null = null

默认情况下 null 和 undefined 是所有类型的子类型。就是说你可以把 null 和 undefined 赋值给 number 类型的变量。

let age: number = null
let realName: string = undefined

但是如果指定了 --strictNullChecks 标记,null 和 undefined 只能赋值给 void 和它们各自,不然会报错。

any、unknown 和 void 类型

  • any

不清楚用什么类型,可以使用 any 类型

let notSure: any = 4
notSure = "maybe a string"     // 可以是 string 类型
notSure = false                // 也可以是 boolean 类型

notSure.name                   // 可以随便调用属性和方法
notSure.getName()

不过,不建议使用 any,不然就丧失了 TS 的意义。

  • unknown

不建议使用 any,当我不知道一个类型具体是什么时,该怎么办?

可以使用 unknown 类型

把 param 定义为 any 类型,TS 就能编译通过,没有把潜在的风险暴露出来,万一传的不是 number 类型,不就没有达到预期了吗。

function divide(param: any) {
  return param / 2;
}

把 param 定义为 unknown 类型 ,TS 编译器就能拦住潜在风险
因为不知道 param 的类型,使用运算符 /,导致报错。

function divide(param: unknown) {
  return param / 2;   // error
}

配合类型断言,可以解决这个问题

function divide(param: unknown) {
  return param as number / 2;
}
  • void

void类型与 any 类型相反,它表示没有任何类型。
比如函数没有明确返回值,默认返回 Void 类型

function welcome(): void {
  console.log('hello')
}

never 类型

never类型表示的是那些永不存在的值的类型。

有些情况下值会永不存在,比如,

  • 如果一个函数执行时抛出了异常,那么这个函数永远不存在返回值,因为抛出异常会直接中断程序运行。
  • 函数中执行无限循环的代码,使得程序永远无法运行到函数返回值那一步。
// 异常
function fn(msg: string): never { 
  throw new Error(msg)
}

// 死循环
function fn(): never { 
  while (true) {}
}

枚举

  • 枚举的意义在于,可以定义一些带名字的常量集合,清晰地表达意图和语义,更容易地理解代码和调试。
  • 常用于和后端联调时,区分后端返回的一些代表状态语义的数字或字符串,降低阅读代码时的心智负担。

基本使用

定义一个数字枚举

enum Color {
  Red,
  Green,
  Blue,
}
console.log(Color.Red) // 0
console.log(Color.Green) // 1
console.log(Color.Blue) // 2
console.log(Color[0]) // Red

特点:

  • 数字递增,枚举成员会被赋值为从 0 开始递增的数字
  • 反向映射,枚举会对枚举值到枚举名进行反向映射,

如果枚举第一个元素赋有初始值,就会从初始值开始递增

enum Color {
  Red=3,
  Green,
  Blue,
}
console.log(Color.Red) // 3
console.log(Color.Green) // 4
console.log(Color.Blue) // 5

手动赋值

定义一个枚举来管理外卖状态,分别有已下单,配送中,已接收三个状态。

enum Status {
  Buy = 1,
  Send,
  Receive
}
console.log(Status.Buy)      // 1
console.log(Status.Send)     // 2
console.log(Status.Receive)     // 3

但有时候后端给你返回的数据状态是乱的,就需要我们手动赋值。
比如后端说 Buy 是 100,Send 是 20,Receive 是 1,就可以这么写:

enum Status {
  Buy = 100,
  Send = 20,
  Receive = 1
}
console.log(Status.Buy)      // 100
console.log(Status.Send)     // 20
console.log(Status.Receive)     // 1

计算成员

枚举中的成员可以被计算

enum FileAccess {
  // constant members
  None,
  Read    = 1 << 1,
  Write   = 1 << 2,
  ReadWrite  = Read | Write,
  // computed member
  G = "123".length
}
console.log(FileAccess.None)       // 0
console.log(FileAccess.Read)       // 2   -> 010
console.log(FileAccess.Write)      // 4   -> 100
console.log(FileAccess.ReadWrite)  // 6   -> 110
console.log(FileAccess.G)       // 3

字符串枚举

字符串枚举意义在于,提供有具体语义的字符串,可以更容易地理解代码和调试。

enum Direction {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT",
}
const value = 'UP'
if (value === Direction.Up) {
    // do something
}

常量枚举

常量枚举编译出来的 JS 代码会简洁很多,提高了性能。常量枚举不允许包含计算成员

const enum Direction {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT",
}
const value = 'UP'
if (value === Direction.Up) {
    // do something
}

会被编译成:

const value = 'UP';
if (value === "UP" /* Up */) {
    // do something
}

不写const会被编译成:

(function (Direction) {
    Direction["Up"] = "UP";
    Direction["Down"] = "DOWN";
    Direction["Left"] = "LEFT";
    Direction["Right"] = "RIGHT";
})(Direction || (Direction = {}));
const value = 'UP';
if (value === Direction.Up) {
    // do something
}

这一堆定义枚举的逻辑会在编译阶段会被删除,常量枚举成员在使用的地方被内联进去。

枚举类型

当枚举作为类型时,表示该属性只能为枚举中的某一个成员

enum SEX{
    man = '男',
    woman = '女',
    unknown = '未知'
}
let arr:Array<SEX> = [SEX.man,SEX.woman]
let s:SEX = SEX.man
console.log(s); // 男

接口

在面向对象语言中,接口(Interfaces)是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类(classes)去实现(implement)。

在ts中,接口除了可用于对类的一部分行为进行抽象以外,也常用于对「对象的形状(Shape)」进行描述。

接口一般首字母大写

定义对象类型

定义对象类型,可以对对象形状进行描述,接口和对象形状必须保持一致

interface IntfPerson {
    name: string;
    age: number;
}

const jack: IntfPerson = {
    name: 'jack',
    age: 25
}

// 缺少或多出属性都会报错
const jack: IntfPerson = {
    name: 'jack'
}
const jack: IntfPerson = {
    name: 'jack',
    age: 25,
    city: '深圳'
}

可选属性

如果不需要完全匹配形状,可以使用可选属性

interface IntfPerson {
    name: string;
    age?: number;
}
const jack: IntfPerson = {
    name: 'jack'
}

只读属性

有时候我们希望对象中的一些字段只能在创建的时候被赋值,那么可以用 readonly 定义只读属性

interface IntfPerson {
    readonly id: number;
    name: string;
    age?: number;
}

let jack: IntfPerson = {
    id: 89757,
    name: 'Tom',
};

// 报错
jack.id = 9527;

描述函数类型

interface 也可以用来描述函数类型

interface ISum {
    (x:number,y:number):number
}

const add:ISum = (num1, num2) => {
    return num1 + num2
}
console.log(add(1,2)); // 3

es6中类的用法

先了解下es6中类的用法:

  • 使用 class 定义类,使用 constructor 定义构造函数
  • 通过 new 生成新实例的时候,会自动调用构造函数。
class Animal {
  public name;
  constructor(name:string) {
      this.name = name;
  }
  sayHi() {
      return `My name is ${this.name}`;
  }
}
let a = new Animal('Kerry');
console.log(a.sayHi()); // My name is Kerry
  1. 类的继承
    使用 extends 关键字实现继承,子类中使用 super 关键字来调用父类的构造函数和方法
class Cat extends Animal {
  constructor(name:string) {
    super(name); // 调用父类的 constructor(name)
    console.log(this.name);
  }
  sayHi() {
    return 'Meow, ' + super.sayHi(); // 调用父类的 sayHi()
  }
}

let c = new Cat('Tom'); // Tom
console.log(c.sayHi()); // Meow, My name is Tom
  1. 存取器
    使用 getter 和 setter 可以改变属性的赋值和读取行为
class Animal {
  constructor(name:string) {
    this.name = name;
  }
  get name() {
    return 'Kerry';
  }
  set name(value) {
    console.log('setter: ' + value);
  }
}

let a = new Animal('Kitty'); // setter: Kitty
a.name = 'Tom'; // setter: Tom
console.log(a.name); // Kerry
  1. 静态方法
    使用 static 修饰符修饰的方法称为静态方法,它们不需要实例化,而是直接通过类来调用:
class Animal {
  static cry() {
    console.log('会哭'); // 会哭
  }
}
Animal.cry(); 

// 报错
const animal = new Animal()
animal.cry() // error

es7中类的用法

  1. 实例属性
    ES6 中实例的属性只能通过构造函数中的 this.xxx 来定义,ES7 提案中可以直接在类里面定义:
class Animal {
  name = 'Kerry';
  constructor() {
    // ...
  }
}

let a = new Animal();
console.log(a.name); // Kerry
  1. 静态属性
    ES7 提案中,可以使用 static 定义一个静态属性:
class Animal {
  static num = 42;

  constructor() {
    // ...
  }
}

console.log(Animal.num); // 42

ts中类的用法

  1. 类修饰符

ts里面定义属性的时候,提供了三种修饰符

public:公有,在类里面、子类、类外面都可以访问
protected:保护类型,在类里面、子类里面可以访问,在类外面没法访问
private:私有,在类里面可以访问,子类、类外部都没法访问

属性如果不加修饰符,默认就是公有public

class Person {
  public name:string
  // protected name:string
  // private name:string
  constructor(name:string){
    this.name = name
  }
  run():string{
    return `${this.name}在运动`
  }
}

class Web extends Person {
  constructor(name:string){
    super(name) // 初始化父类的构造函数
  }
  run():string{
    return `${this.name}在运动-子类`
  }
  work(): string {
    return `${this.name}在工作`
  }
}
const w = new Web('kerry')
console.log(w.run()); // kerry在运动-子类
console.log(w.work()); // kerry在工作
console.log(w.name); // kerry
  1. 抽象类
  • 抽象类是不允许被实例化
abstract class Animal {
  public name;
  public constructor(name:string) {
    this.name = name;
  }
}
let a = new Animal('Kerry'); // error
  • 抽象类中的抽象方法必须被子类实现
abstract class Animal {
  public name;
  public constructor(name:string) {
    this.name = name;
  }
  public abstract sayHi():void;
}

class Cat extends Animal {
  public sayHi() {
    console.log(`Meow, My name is ${this.name}`);
  }
}
let cat = new Cat('Kerry');
  • 给类添加类型
class Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  sayHi(): string {
    return `My name is ${this.name}`;
  }
}

let a: Animal = new Animal('Kerry');
console.log(a.sayHi()); // My name is Kerry

接口和类

类实现接口

一般来讲,一个类只能继承自另一个类,有时候不同类之间可以有一些共有的特性,这时候就可以把特性提取成接口(interfaces),
用 implements 关键字来实现,大大提高了面向对象的灵活性

这里举个例子:
门是一个类,防盗门是门的子类。如果防盗门有一个报警器的功能,我们可以简单的给防盗门添加一个报警方法。这时候如果有另一个类,车,也有报警器的功能,就可以考虑把报警器提取出来,作为一个接口,防盗门和车都去实现它

interface Alarm {
    alert(): void;
}

class Door {
}

class SecurityDoor extends Door implements Alarm {
    alert() {
        console.log('SecurityDoor alert');
    }
}

class Car implements Alarm {
    alert() {
        console.log('Car alert');
    }
}

一个类可以实现多个接口

interface Alarm {
    alert(): void;
}

interface Light {
    lightOn(): void;
    lightOff(): void;
}

class Car implements Alarm, Light {
    alert() {
        console.log('Car alert');
    }
    lightOn() {
        console.log('Car light on');
    }
    lightOff() {
        console.log('Car light off');
    }
}

接口继承接口

interface Alarm {
    alert(): void;
}

interface LightableAlarm extends Alarm {
    lightOn(): void;
    lightOff(): void;
}

class Car implements LightableAlarm {
    lightOn(): void {
        console.log('Car light on');
    }
    lightOff(): void {
        console.log('Car light off');
    }
    alert(): void {
        console.log('Car light on');
    }
}

类型断言和类型推论

类型断言

语法:值 as 类型
使用断言,目的是告诉ts,我比你更清楚这个参数是什么类型,以此防止报错

function getLength(arg: number | string): number {
    const str = arg as string
    if (str.length) {
        return str.length
    } else {
        const number = arg as number
        return number.toString().length
    }
}
console.log(getLength(123)); // 3
console.log(getLength("123")); // 3

// 需要注意的是,如果把一个类型断言成联合类型中不存在的类型则会报错
function getLength(arg: number | string): number {
    return (arg as number[]).length // error
}

类型推论

ts里,在有些没有明确指出类型的地方,类型推论会帮助提供类型。

  • 定义时不赋值,ts会自动推导成 any 类型
let a
a = 18
a = 'hello'
  • 初始化变量
// 因为赋值的时候赋的是一个字符串类型,所以ts自动推导出userName是一个string类型。
let userName = 'lin'
// userName = 123  //再修改时会报错
  • 设置默认参数值
// 会自动推导为number类型
function printAge(num = 18) {
    console.log(num)
    return num
}
printAge(20)
// printAge("Kerry") // error
  • 函数返回值
// 定义没有返回值的函数,会自动推导返回值为void类型
function welcome() {
    console.log('hello')
}
welcome()

联合类型、交叉类型和类型别名

  • 联合类型 | 是指可以取几种类型中的任意一种,而交叉类型 & 是指把几种类型合并起来。
  • 交叉类型和 interface 的 extends 非常类似,都是为了实现对象形状的组合和扩展。

联合类型

如果希望一个变量可以支持多种类型,就可以用联合类型(union types)来定义。

例如,一个变量既支持 number 类型,又支持 string 类型

// 联合类型大大提高了类型的可扩展性
let num: number | string | boolean

num = 8
num = 'eight'
num = true

交叉类型

如果要对对象形状进行扩展,可以使用交叉类型 &
比如 Person 有 name 和 age 的属性,而 Student 在 name 和 age 的基础上还有 grade 属性
这和类的继承是一模一样的,这样 Student 就继承了 Person 上的属性,

interface Person {
    name: string
    age: number
}

type Student = Person & { grade: number }

类型别名

类型别名,就是给类型起个别名。类型别名用 type 关键字来书写,有了类型别名,我们书写 TS 的时候可以更加方便简洁。
比如这个例子,getName 这个函数接收的参数可能是字符串,可能是函数

type Name = string
type NameResolver = () => string
type NameOrResolver = Name | NameResolver          // 联合类型
function getName(n: NameOrResolver): Name {
    if (typeof n === 'string') {
        return n
    }
    else {
        return n()
    }
}
// 调用
console.log(getName('kerry')); // kerry
console.log(getName(() => 'kerry')); // kerry

type 和 interface

能用interface就用interface,实现不了再考虑用type

相同点1:都可以描述一个对象或者函数

  • 描述一个对象
type IntfPerson = {
    name: string
    age: number
}
interface IntfPerson {
    name: string
    age: number
}

const person: IntfPerson = {
    name: 'kerry',
    age: 18
}
  • 描述一个函数
type addType = (num1:number,num2:number) => number

interface addType {
    (num1:number,num2:number):number
}
const add:addType = (num1, num2) => {
    return num1 + num2
}

相同点2:都可以实现继承

interface 使用 extends 实现继承, type 使用交叉类型实现继承

我们定义一个 IntfPerson 类型和 Student 类型,Student 继承自 IntfPerson

// interface 继承 interface
interface Person { 
    name: string 
  }
  interface Student extends Person { 
    grade: number 
  }

// type 继承 type
type IntfPerson = { 
    name: string 
}
type Student = IntfPerson & { grade: number  }   // 用交叉类型


// interface 继承 type
type IntfPerson = { 
    name: string 
}

interface Student extends IntfPerson { 
    grade: number 
}


// type 继承 interface
interface IntfPerson { 
    name: string 
}

type Student = IntfPerson & { grade: number  }   // 用交叉类型

不同点1:type 可以声明基本类型、联合类型、交叉类型、元组,interface 不行

基本类型
type Name = string
// 联合类型
type Name = string | number
// 元组
type Name = [string, number]
交叉类型
interface IntfPerson { 
    name: string 
}
type Student = IntfPerson & { grade: number  }

let myName:Name = 'kerry'
let myName:Name = 123
let myName:Name = ['kerry',2]
let kerry:Student = {
    name:'kerry',
    grade: 1
}

不同点2: interface可以声明合并,type不行

interface User{
    nage: string,
    age: number,
}
interface User{
    sex: string
}

let user: User = {
    nage: '小明',
    age: 10,
    sex: '男'
}

泛型

泛型是指在定义函数、接口或类的时候,不预先指定具体类型,而是在使用的时候再指定类型。可以使得输入输出类型统一,且可以输入输出任何类型。

基本用法

定义一个print函数,传入一个string类型参数并返回一个string类型

  • 不使用泛型:
function print(arg:string):string {
    console.log(arg)
    return arg
}

假如这时候又多了一个number类型,则需要这样写

function print(arg:string | number):string | number {
    console.log(arg)
    return arg
}
const result:string | number = print(123)
console.log('result',result); // 123
  • 使用泛型解决
function print<T>(arg:T):T {
    console.log(arg)
    return arg
}

// 两种方式使用:
// 方式1:定义要使用的类型
const result = print<number>(123)

// 方式2:使用ts类型自动推断
const result = print(123)

接口中使用泛型,定义函数类型

interface IGeneric<T> {
    (arg: T): void
}
  
function fn<T>(arg: T): void {
    console.log(arg);
}
  
let myFn: IGeneric<number> = fn;
myFn(13); //13

在类中使用泛型

定义一个栈,有入栈和出栈两个方法,如果想入栈和出栈的元素类型统一,写法如下:

class Stack<T> {
    private data: T[] = []
    push(item:T) {
        return this.data.push(item)
    }
    pop():T | undefined {
        return this.data.pop()
    }
}

入栈和出栈是number类型
const s1 = new Stack<number>()
s1.push(1)
console.log(s1.pop()); // 1

泛型约束

假设现在有这么一个函数,打印传入参数的长度

function printLength<T>(arg: T): T {
    console.log(arg.length) // 因为不确定 T 是否有 length 属性,会报错
    return arg
}
printLength([1,2,3])

结合interface,通过extends来实现泛型约束

interface ILength {
    length: number
}
function printLength<T extends ILength>(arg: T): T {
    console.log(arg.length)
    return arg
}
printLength([1,2,3])

装饰器

定义

  • 装饰器是一种特殊类型的声明,它能够被附加到类声明、方法、属性或参数上,可以修改类的行为。
  • 通俗讲装饰器就是一个方法,可以注入到类、方法、属性参数上来扩展类、属性、方法、参数的功能
  • 常见的装饰器有:类装饰器、属性装饰器、方法装饰器、参数装饰器
  • 装饰器的写法:普通装饰器(不可传参)、装饰器工厂(可传参)

类装饰器:普通装饰器

function logClass(parmas:any){
  parmas.prototype.apiUrl = "http://www.baidu.com"
}

@logClass
class HttpClient {
  constructor(){
  }

  getData(){

  }
}

const http:any = new HttpClient()
console.log(http.apiUrl) // http://www.baidu.com

类装饰器:装饰器工厂

function logClass(parmas:string){
  return function(target:any){
    console.log(parmas)
    console.log(target)
  }
}

@logClass('hello')
class HttpClient {
  constructor(){
  }

  getData(){

  }
}

console打印出来结果:

hello
index.js:134 class HttpClient {
    constructor() {
    }
    getData() {
    }
}

属性装饰器

接收2个参数,target当前类的原型对象,attr当前属性名称

function logClass(parmas:string){
  return function(target:any){
    console.log(parmas)
    console.log(target)
  }
}

function logProperty(params:string){
  return function(target:any,attr:any){
    console.log(target) // 当前类的原型对象
    console.log(attr) // url 
    // 修改属性值
    target[attr] = params
  }
}

@logClass('hello')
class HttpClient {

  @logProperty('http://api.xxx.com')
  public url: string | undefined
  constructor(){
  }

  getData(){
    console.log(this.url) // http://api.xxx.com
  }
}

const http = new HttpClient()
http.getData()

方法装饰器

方法装饰器接收三个参数:1. target当前类原型的对象 2. 当前方法名  3. 当前方法的描述符

function get(params:string){
  return function(target:any,methodName:any,desc:any){
    console.log(target) // 当前类的原型对象
    console.log(methodName) // 方法名 
    console.log(desc) // 描述符 
    // 和类装饰器一样,可以扩展当前类属性和方法
    target.apiUrl='xxxx'
    target.run=function(){
      console.log('run')
    }

    // 修改装饰器的方法  把装饰器方法里传入的所有参数改为string类型
    // 1. 保存当前方法 2. 参数处理并调用oMethod方法
    const oMethod = desc.value
    desc.value = function(...args:any[]){
      args = args.map((value)=>{
        return String(value)
      })
      // 调用oMethod方法,并将参数args传入
      // 与call不同,call接受参数列表,apply接受数组形式参数
      oMethod.apply(this,args)
    }
  }
}

class HttpClient {
  public url: string | undefined
  constructor(){
  }

  @get('http://api.xxx.com')
  getData(...args:any[]){
    console.log(args); // ['123', '122']
    console.log('我是getData里面的方法')
  }
}

const http:any = new HttpClient()
console.log(http.apiUrl);
http.run()

http.getData(123,'123')

方法参数装饰器

参数装饰器会在运行时当做函数被调用,可以使用参数装饰器为类的原型增加一些元素数据,传入3个参数:

  1. 对于静态成员来说,是类的构造函数,对于实例成员是类的原型对象
  2. 方法的名字
  3. 参数在函数参数列表中的索引
function logParams(params:any){
  return function(target:any,methodName:any,paramsIndex:any){
    console.log(params); // uuid
    console.log(target); // {constructor: ƒ, getData: ƒ}
    console.log(methodName); // getData
    console.log(paramsIndex); // 0

    // 给类的原型增加属性
    target.apiUrl = params
  }
}

class HttpClient {
  public url: string | undefined
  constructor(){
  }

  getData(@logParams('uuid') uuid:any){
    console.log(uuid);
  }
}

const http:any = new HttpClient()
http.getData(123456)
console.log(http.apiUrl); // uuid

装饰器执行顺序

function logClass1(parmas:string){
  return function(target:any){
    console.log('类装饰器1')
  }
}

function logClass2(parmas:string){
  return function(target:any){
    console.log('类装饰器2')
  }
}

function logAttr(parmas?:string){
  return function(target:any,attrName:any){
    console.log('属性装饰器')
  }
}

function logMethod(parmas?:string){
  return function(target:any,methodName:any,desc:any){
    console.log('方法装饰器')
  }
}

function logParams1(parmas?:string){
  return function(target:any,methodName:any,paramsIndex:any){
    console.log('方法参数装饰器1')
  }
}

function logParams2(parmas?:string){
  return function(target:any,methodName:any,paramsIndex:any){
    console.log('方法参数装饰器2')
  }
}

@logClass1('hello')
@logClass2('hello')
class HttpClient {

  @logAttr()
  public apiUrl:string | undefined
  constructor(){
  }

  @logMethod()
  getData(){

  }

  setData(@logParams1() attr1:any,@logParams2() attr2:any){

  }
}

const http = new HttpClient()

执行以上代码,可以看到执行顺序是:

属性>方法>方法参数>类

如果有多个同样的装饰器,它会先执行后面的

声明文件和内置对象

声明文件

当使用第三方库时,很多三方库不是用 TS 写的,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。

  • declare

在ts中如果直接使用Vue,就会报错

const app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})

这时,我们可以使用 declare 关键字来定义 Vue 的类型,这样就不会报错了

interface VueOption {
    el: string,
    data: any
}

declare class Vue {
    options: VueOption
    constructor(options: VueOption)
}

const app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})
  • .d.ts文件

通常我们会把声明语句放到一个单独的文件(xxx.d.ts)中,这就是声明文件,以 .d.ts 为后缀。
一般来说,ts 会解析项目中所有的 *.ts 文件,当然也包含以 .d.ts 结尾的文件。

  • 使用三方库

在使用第三方库的时候,如果社区已经为其提供了对应的类型文件,那么我们就不需要自己再声明一次了,直接安装即可

例如:

// 安装 lodash 的类型包
npm install @types/lodash -D

内置对象

JavaScript中有很多内置对象,它们可以直接在 TypeScript 中当做定义好了的类型。
内置对象是指根据标准在全局作用域 global 上存在的对象,这里的标准指的是 ECMAcript 和其他环境(比如DOM)的标准。

  1. ECMAScript的内置对象
    Array、Boolean、Number、String、Date、RegExp、Error
const nums: Array<number> = [1,2,3]

const date: Date = new Date()

const err: Error = new Error('Error!');

const reg: RegExp = /abc/;
  1. BOM和DOM的内置对象
    Window、Document、HTMLElement、DocumentFragment、Event、NodeList
let body: HTMLElement = document.body

let allDiv: NodeList = document.querySelectorAll('div');

document.addEventListener('click', (e: MouseEvent) => {
    e.preventDefault()
  // Do something
});

[

](http://localhost:3000/)

ts实战

我们使用react+ts实现一个todo List功能

功能包括:

  • 查看任务列表
  • 添加、删除任务
  • 完成任务

先看效果图

image.png

初始化一个react项目

https://create-react-app.dev/

  1. 初始化一个typescript项目
yarn create react-app react-ts-demo --template typescript
  1. 运行项目
cd react-ts-demo & yarn start
  1. 浏览器访问:http://localhost:3000/

代码实现

  1. 创建一个父组件todolist
export interface ITodoItem {
    taskName: string,
    status: TaskStatus
}

// React.FC表示react的一个函数组件,是React.FunctionComponent的简写形式
const TodoList:React.FC = ()=>{

  const [list, setList] = useState<Array<ITodoItem>>([])
  const [inputValue, setInputValue] = useState<string>("")

  const updateList = (list:ITodoItem[])=>{
    setList(list)
    saveTaskList(list)
  }

  const handleAdd = ()=>{
    if(!inputValue.trim()){
        return alert("输入不能为空")
    }
    let [...newList] = list;
    newList.push({
        taskName: inputValue,
        status: TaskStatus.PENDING
    })
    updateList(newList)
    setInputValue("")
  }


  const handleStart = (index:number)=>{
    let [...newList] = list;
    newList = newList.map((item,indx)=>{
        if(indx === index){
            return {
                ...item,
                status: TaskStatus.IN_PROGRESS
            }
        }
        return item
    })
    updateList(newList)
  }

  const handleComplete = (index:number)=>{
    let [...newList] = list;
    newList = newList.map((item,indx)=>{
        if(indx === index){
            return {
                ...item,
                status: TaskStatus.COMPLETED
            }
        }
        return item
    })
    updateList(newList)
  }


  const handleDel = (index:number)=>{
    let [...newList] = list;
    newList.splice(index,1)
    updateList(newList)
  }

  // 初始化列表数据
  useEffect(() => {
    const list = getTaskList()
    setList(list)
  }, [])
  
  
  return (
    <div className={styles.container}>
        <h1 className={styles.title}>TODO LIST</h1>
        <div className={styles.form}>
            <input className={styles.textInput} type="text" value={inputValue} placeholder="输入任务名" onChange={(e)=>setInputValue(e.target.value)}/>
            <button className={styles.addBtn} onClick={handleAdd}>添加任务</button>
        </div>
        <div className={styles.list}>
            {list.map((item,index)=> {
                return <TodoItem key={index} index={index} item={item} handleStart={handleStart} handleComplete={handleComplete} handleDel={handleDel}/>
            })}
        </div>
    </div>
  )
}

export default TodoList;
  1. 创建一个子组件todoItem并在父组件中引入
interface IProps {
    index: number
    item: ITodoItem
    handleStart: (index: number) => void
    handleComplete: (index: number) => void
    handleDel: (index: number) => void
}

// 对于接收的组件参数,需要定义参数类型
const TodoItem:React.FC<IProps> = ({index,item,handleStart,handleComplete,handleDel})=>{
    return (
        <div className={styles.taskItem}>
            <div className={styles.name}>{item.taskName}</div>
            <div className={styles.statusText}>
                {item.status===TaskStatus.PENDING && <span className={styles.pending}>待开始</span>}
                {item.status===TaskStatus.IN_PROGRESS && <span className={styles.inProgress}>进行中</span>}
                {item.status===TaskStatus.COMPLETED && <span className={styles.completed}>已完成</span>}
            </div>
            <div className={styles.btns}>
                {item.status===TaskStatus.PENDING && (
                    <button className={styles.btn} onClick={()=>handleStart(index)}>开始</button>
                )}
                {item.status===TaskStatus.IN_PROGRESS && (
                    <button className={styles.btn} onClick={()=>handleComplete(index)}>完成</button>
                )}
                <button className={styles.btn} onClick={()=>handleDel(index)}>删除</button>
            </div>
        </div>
    )
}

export default TodoItem
  1. 数据保存和获取

我们在第一次进入的时候获取初始化数据

const getTaskList = ()=> {
    const taskList = localStorage.getItem("taskList")
    if(taskList){
        return JSON.parse(taskList)
    }
    return []
}

// 初始化列表数据
  useEffect(() => {
    const list = getTaskList()
    setList(list)
  }, [])

添加、完成以及删除的时候保存和更新数据

const saveTaskList = (list: ITodoItem[])=>{
    localStorage.setItem('taskList',JSON.stringify(list))
}

const updateList = (list:ITodoItem[])=>{
    setList(list)
    saveTaskList(list)
  }

上面代码我已经上传到github,欢迎star👏🏻

参考阅读

posted @ 2022-04-09 12:44  fozero  阅读(1704)  评论(0编辑  收藏  举报