从0到1学习typescript

85 阅读15分钟

前言

对于前端人来说TypeScript应该不陌生,它的重要性不言而喻。其实本人也系统学习过TypeScript几次了,但是从来没有认真的记录过学习过程,正好因为疫情封闭在家,学习过程中正好记录下,从0到1,从简单到高级,咱们一步一步来

基本类型

string类型

let str: string // √
str = '2'
let str1 = '1'  // √

str1 = 1 // ×

上述是关于字符串类型的书写,可以通过一个规律表现出:冒号后面的就是类型描述,通过上述代码我们也可以发现一些规律

  • 我们可以通过:string 来描述string类型
  • 如果存在一些默认值,我们可以通过推导得到类型.例如:let str1 = '1'
  • 如果一个变量一旦标记为一种类型,就不可以将另一种类型的值赋过去
  • 上述的解释,合适不复杂的所有类型,下面就不做过多的阐述了

string类型特殊讲解

let a: string = '1' // √
let b: String = '1' // √

let b: String = new String('2') // √
let c: string = new String('2') // ×
  • 上述的实例讲述了在声明string类型以及String类型有什么不同
    • 首先string用来声明基本数据类型,String是包装对象数据类型
    • 因为字符串1 本身也是包装类型String的实例,所以let b: String = '1'是对的,但是反过来不行

boolean类型

let flag: boolean
flag = true

let flag1 = false

number 类型

let num: number
num = 1

let num1 = 2

联合数据类型

一般我们进行标记的时候,一个变量会被标记一种类型,但是如果真的在业务中出现了一个变量需要多种数据类型怎么办呢??? 这个时候我们就可以使用联合数据类型

let a: number | string
a = '1'
a = 1

let b: number | string | boolean
  • 我们可以用符号|来描述多种数据类型,表示既可以赋值为A类型也是可以赋值为B类型

字面量类型

使用联合类型的时候,还可以定义字面量类型

let IType = 'up' | 'down' | 'left' | 'right'

const a: IType = 'up' // √
const b: IType = 'up1' // ×

数组类型

我们同样可以给数组进行类型标记

let arr: number[] = [1, 2, 3] // √
let arr1: Array<number> = [1, 2, 3] // √

let arr2: number[] = [1, 2, '3'] // ×
  • 我们可以给数组标记类型,在真实的业务中一组数组都是具有一种类型,就好比上述实例
  • 但是标记数组类型有两种方式,就是上述两种
  • 数组中的值类型只能是标记的类型,如果出现不同的类型就会出错,例如上述最后一个实例
  • 如果业务中出现了数组需要存在多种数据类型怎么办呢???那肯定是用联合类型. const arr3: (string | number)[] = []

元组类型

标记数组类型,无非固定指定位置是什么类型,但是我们的元组可以。可以将元组理解为数组的另一种表现形式

let arr: [string, number] = ['1', 1] // √
arr.push(2)
console.log(arr[2]) // ×

let arr1: [string, number] = [1, 1] // ×
console.log(arr1[2]) // √
  • 元组的声明可以规定指定位置指定类型
  • 只能通过下标来获取值
  • 元素不能扩展,就算扩展后也不能获取超出初始下标长度的值
  • 而且赋值的类型位置必须一一对应起来
  • 元组在一般的业务中用途较少,但是在复杂类型中能获取到比较好的效果,在后续的实例中会慢慢讲述

null 以及undefined类型

let a: null = null
let b: undefined = undefined
  • 使用相对简单,不做过多的说明

any类型

  • TypeScript中如果没有声明类型的话,默认就是any类型
  • 如果是标注了any类型,就是默认跳过类型提示。原则上不允许标注any类型。但是一些特殊场合下也可以使用
  • 也可以在配置文件中配置允许使用any
  • any类型可以赋值给任意类型,除了never类型除外
let a: any = 1

let b: string = a
let c: boolean = a

// 特殊场合使用`any`类型
interface IFn {
  (value: string, ...args: any[]): void
}

never类型

  • 我将never类型理解为不可到达类型,理解为不存在类型
  • 类型never 是任意类型的子类
function test(): never {
	while(true) {}
}

let test1: never = test()

枚举类型

一般我们在使用某个对象的XXX类型的时候,可以使用枚举类型

enum A {
	add, // add => 0
	minus, // minus => 1
	mult, // mult => 2
	divis // divis => 3
}

const enum A {
	add, // add => 0
	minus, // minus => 1
	mult, // mult => 2
	divis // divis => 3
}

enum B {
	add, // add => 0
	minus = 'minus', // minus => minus
	mult, // ERROR 报错
	divis
}

enum C {
	add, // add => 0
	minus = 'minus', // minus => minus
	mult = 1, // mult => 1
	divis // divis => 2
}
  • 上述列举了不同情况下的枚举使用情况
    • 声明枚举类型的时候,ts会默认添加值,默认是从0开始
    • 如果中途一旦出现赋值了非数字的值,后续的必须都进行赋值,例如实例3
    • 如果中途又赋值了数字值,之后的ts可以自己推导
    • 普通枚举以及常量枚举有什么不同呢??? 普通枚举编译后会成为对象,但是常量枚举在编译后会成为替换的值

断言

在这里先进行阐述下,断言一般分为两种as 以及!。 强行断言以及非空断言。那我们一般在什么场合下使用他们呢

as 断言

// 案例1:
let n: string | number

// 如果此时我们调用`变量n`上的方法,只能调用string以及number上共同方法,那就想调用string上的方法呢???
(n as string).startsWith('http')

// 实例2
let arr = Array.from({length: 3}).map(() => {
	return {name: 'test', age: 22} as {name: string, age: number}
})

// 实例3
const app: HtmlElement | null = document.getElementById('app')

// 使用1
app?.style.background = 'red'
// 使用2
(app as HtmlElement).style.background = 'red'

非空断言

还可以使用!非空断言,其实就是告诉代码该值一定有值。

const app: HTMLElement | null = document.getElementById('app')
app!.style.background = 'red'

交集 以及并集

  • 提到交集以及并集会想到两个关键字& 以及| . 一般我们从字面量来看,符号&一定是并集,因为在js中是且的意思。而|是交集的意思,但是我们在ts中正好相反
  • & 是交集的意思。是两种都具备的意思. 例如:高帅的人 & 富的人 = 高富帅
  • | 是并集的意思,既包含A 又包含B的意思
type A = {
	name: string
}
type B = {
	age: number
}
type C = A & B
const c: C = {name: 'test', age: 10}

type PersonA = {name: string, age: number}
type PersonB = {name: string, work: boolean}

type PersonC = PersonA | PersonB

函数的类型声明

  • 在js中函数的定义方式分为两种:一种函数声明式定义function test() {}, 另一种是函数赋值式定义const fn = () => {}

函数类型声明方式

// 实例1
function test(a: string, b: string): string {
  return a + b
}
// 同样可以 => 
function test(a: string, b: string) {
	return a + b
}
// 后者属于 类型推导

// 实例2
interface IFn {
	(a: string, b: string): string
}
const test1: IFn = (a, b) => a + b

// 实例3
const test1: (a: string, b:string) => string = (a: string, b: string) => a + b
  • 实例3中分为两部分内容,一种类型描述,另一种是函数赋值
    • (a: string, b:string) => string 表示函数声明
    • (a: string, b: string) => a + b 表示函数赋值
    • 后者必须满足前者的类型要求,也需要重复声明

函数扩展运算符

function test2(a: string, ...args: string[]) {}

函数的可选参数

function test3(a: string, b?: string) {}
  • 虽然不赋值也是默认undefined,但是类型声明中可选类型跟undefined是两回事
  • 可选类型表示可传可不传,但是如果传递一定是string类型

函数重载

function test(a: string, b: string): string {
  return a + b
}

const test1: (a: string, b: string) => string = (a: string, b: string) => a + b

function test2(a: string, ...args: string[]) {}

function test3(a: string, b?: string) {}

function sum(a: string, b: string): string
function sum(a: number, b: number): number
function sum(a: string | number, b: string | number): string | number {
  if (typeof a === 'string') {
    return a + b
  } else {
    return a + Number(b)
  }
}

sum(1, 1)
export {}
  • 函数重载:实现参数不同的话,实现的逻辑不同

class类

构造函数描述

可以对class类进行数据类型描述,可以描述属性,方法,以及构造函数

// 修改前1
class Person {
  constructor(name: string, age: string) {
    this.name = name // error Property 'age' does not exist on type 'Person'
    this.age = age // error
  }
}

// 修改后1
class Person {
  constructor(public name: string, public age: string) {
    this.name = name // success
    this.age = age // success
  }
}

// 修改后2
class Person2 {
  name: string
  age: string
  constructor(name: string, age: string) {
    this.name = name
    this.age = age
  }
}
  • 在构造函数声明属性的时候,如果给this上进行赋值,需要提前声明,可以使用public 来声明,简单快捷。同时也可以定义数据类型

super 实现继承

class Man {
  constructor(public name: string) {
    this.name = name
  }
}
class Person3 extends Man {
  constructor(name: string, public age: string) {
    super(name)
    this.age = age
  }
}
  • 如果实现继承的话,在constructor中必须调用super

属性修饰符

  • readonly 设置属性只读,不可修改
  • private 设置私有属性,只能自己访问,不能其他类访问。例如:子类不能访问
  • public 设置共同属性,就是默认属性公开可以访问
  • protected 受保护的属性,只允许自己以及子类进行访问
class Man {
  public name: string
  readonly age: number
  protected school: string = '学校'
  constructor(name: string, public test: string) {
    this.name = name
    this.age = 10
  }
}

class Man1 {
  private constructor(public name: string) {
    this.name = name
  }
}

const man1 = new Man1('tset') // error
  • 修饰符private可以标识constructor. 但是这样的话就无法进行new

类的原型方法/ 静态方法/ 属性等

class Cat {
  constructor(public name: string) {
    // 实例属性
    this.name = name
  }

  // 原型函数
  eat(kind: string): string {
    return kind
  }

  // 静态函数
  static say(): string {
    return 'say'
  }

  // 实例属性
  get age(): number {
    return 10
  }
}

抽象类

  • 抽象方法中必须定义在抽象类中
  • 如果函数继承抽象类,必须实现抽象方法
abstract class Woman {
  abstract eat(kind: string): string

  say(content: string): string {
    return content
  }
}

class Person3 extends Woman {
	// 子类实现父类的方法
  eat(kind: string): string {
    return kind
  }
}

interface 接口 以及type

一般进行类型描述的时候,使用interface以及type居多,这里先对两者进行比较,再一一列举interface的用处

  • type 可以使用联合类型,但是interface不可以使用
  • type 可以使用关键字in,但是interface不可以
  • type 不可以进行继承实现,但是interface可以通过继承以及是实现

interface 实现基本的描述

interface IPerson {
  name: string
  age: number
}
const person: IPerson = {
  name: '',
  age: 10
}

接口的可选/ 可获取/ 可索引/ 继承

// 可选属性
interface ITest {
  name: string
  age?: number
}

// 可获取
interface IPerson1 {
  [keyName: string]: any
}

// 可索引
interface IPerson2 {
  [keyName: number]: any
}

// 继承
interface A {
  name: string
}

interface B extends A {
  age: number
}
let b: B = {
  age: 10,
  name: ''
}

接口描述类

以插件mysql-qs-parse为例,进行类的描述

declare class SqlParse {
  public db: Connection | null
  constructor(host: string, user: string, password: string, database: string)
  constructor(host: IConnectConfigOptions, user?: string, password?: string, database?: string)

  open(): Promise<Connection>
  release(): void
  query(sql: string): Promise<any>
  size(tableName: string, where?: IRecords): Promise<number>
  findOne(fields: IFieldOptions[], tableName: string, where: IRecords): Promise<IRecords>
  find(fields: IFieldOptions[], tableName: string, where?: IRecords): Promise<IRecords[]>
  find(fields: IFindOptions): Promise<IRecords[]>
  insert(fields: IRecords, tableName: string): Promise<number>
  update(fields: IRecords, tableName: string, where: IRecords): Promise<number>
  on(keyName: string, fn: IFn): void
  once(keyName: string, fn: IFn): void
  emit(keyName: string, ...args: any[]): void
  off(keyName: string, fn: IFn & { l?: IFn }): void
}

interface MysqlParse {
  new (host: string, user: string, password: string, database: string): SqlParse
  new (host: IConnectConfigOptions, user?: string, password?: string, database?: string): SqlParse
}
  • 我们可以用接口描述类的构造函数
  • class类本身可以描述实例本身

泛型

泛型可以描述一类类型

  • 泛型可以广义上定义某种属性
const mapArray = <T>(value: T, times: number) => {
  const arr = []

  for (let i = 0; i < arr.length; i += 1) {
    arr.push(value)
  }
  return arr
}
const res = mapArray<string>('111', 3)
const res1 = mapArray<number>(111, 3)
const res2 = mapArray(true, 3)
  • 泛型可以对类型进行约束, 下面的实例中显示T 必须满足对象中包含属性length
const getLen = <T extends {length: number}>(obj: T): number {
  return obj.length
}

const getValue = <T extends object, K extends keyof T>(obj: T, key: K) => {
  return obj[key]
}
  • 泛型的不同位置,意义不同
// 实例1
interface IMapArray {
	<T>(value: T, times: number): T[]
}

// 实例2
interface IMapArray<T> {
	(value: T, times: number): T[]
}
  • 实例1 是在使用的时候调用泛型
  • 实例2 是在定义接口的时候使用泛型

keyof

keyof 获取类型的key值,不同的类型情况不同

interface IPerson {
  name: string,
  age: number
}
type A = keyof IPerson // name | age

type B = keyof string // string属性 例如:toString等

type C = keyof any // number | string | symobl

type D = keyof never // number | string | symobl

type E = keyof unknown // never

export {}

内置类型

很多数据类型判断都是基于类型分发。

interface Bird {
  name: '鸟'
}
interface Sky {
  name: '蓝色'
}
interface Fish {
  name: '鱼'
}
interface Water {
  color: '透明'
}
type MyType<T extends Bird | Fish> = T extends Bird ? Sky : Water
type MyType1<T extends Bird | Fish> = [T] extends Bird ? Sky : Water

type IEnv = MyType<Bird> // Sky

// -- 内容分发 分别拿联合类型中每个值 都进行判断
type IEnv1 = MyType<Bird | Fish> // Sky | Wather
type IEnv2 = MyType1<Bird | Fish> // Wather
  • 内容分发的必要条件:
    • 泛型中使用联合类型
    • 必须是裸体 例如:MyType1

Exclude

排除

// 此时T 触发了内容分发。T中的类型逐一跟K进行extends判断
type Exclude<T, K> = T extends K ? never : T

type F = Exclude<string | number | boolean, boolean>

Extract

抽离

type Extract<T, K> = T extends  K ? T : never

type F1 = Extract<string | number | boolean, boolean | string>

NonNullable

非空判断

type NonNullable<T> = T extends null | undefined ? never : T

type F2 = NonNullable<string>

Partial

转换为非必输

interface F3  {
  name: string,
  age: number
}

type Partial<T> = {
  [P in keyof T]?: T[P]
}
type F4 = Partial<F3>
  • 但是这个Partial 有个问题就是只能设置表面一层,不能进行深度的转换,接下来会介绍一个DeepPartial

DeepPartial

深度转换为非必输入

interface F5 {
  name: string,
  age: number,
  school: {
    name: string,
    address: string
  }
}
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]>: T[P]
}
type F6 = DeepPartial<F5>

Readonly

设置只读属性

interface F7 {
  name: string,
  age: number
}
type Readonly<T> = {
  readonly [P in keyof T]: T[P]
}
type F8 = Readonly<F7>

Required

设置为必须属性

interface F9 {
  name?: string,
  age?: number
}
type Required<T> = {
  [P in keyof T]-?: T[P]
}
type F10 = Required<F9>

Pick

挑选,摘取,千挑万选

interface F13 {
  name: string,
  age: number,
  address: string
}
type Pick<T, K extends keyof T> = {
  [P in K]: T[P]
}
type F14 = Pick<F13, 'name'>

Omit

进行忽略,对指定的数据类型进行忽略

interface F11 {
  name: string,
  age: number,
  address: string
}

type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>

type F12 = Omit<F11, 'name'>

PartPartial

实现部分可选,部分不可选

// 部分可选 部分不可选
interface F15 {
  name:string,
  age: number,
  address: string
}

type PartPartial<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
type F16 = PartPartial<F15, 'name'>

Merge

将两个对象类型进行合并,如果key相同,value不同的话取后者

// merge 实现
const fn = <T extends object, K extends object>(a: T, b: K): Omit<T, Extract < keyof T, keyof K>> & K => {
  return {...a, ...b}
}
const res = fn({a: 1, b: 2}, {a: '1', c: 2})

兼容性

ts在类型处理过程中,有一定的兼容性处理,在思考过程中一切都是基于“安全”的考虑

联合类型兼容性

type P1 = string | number
type P2 = string
let a!:P1
let b!: P2
b = a // error
a = b // success
  • 上述的实例中赋值b = a会出错,但是a = b不会出现
    • 因为a是一个联合类型,a有可能是number/ string。但是b只能接受string类型。所以是不安全的
    • 但是a = b就不会
  • 总结:赋值过程中,少的可以给多的赋值,但是多的不能给少的赋值

interface接口兼容性

// interface兼容性
interface P3 {
  name: string,
  age: number
}
interface P4 {
  name: string
}
let c!: P3
let d!: P4
c = d // error
d = c // success
  • 上述是interface兼容性的时候,c = d赋值会报错。但是d = c没有错。说明:被赋值的类型范围比赋值的类型范围大。是安全的。
    • 原因是类型P4只有数据类型name. 但是P3 具有属性name, age. 就是可以给“我”多赋值,但是不能少赋值

函数的兼容性

参数兼容性
type IFn1 = (a: string, b: string) => string
type IFn2 = (a: string) => string

let t1!: IFn1
let t2!: IFn2
t1 = t2 // success
t2 = t1 // error

个人觉得从安全以及运行的角度来分析比较容易记忆

  • 上述实例中t2 = t1赋值时出错的, 假如在未赋值之前调用函数t2 其实需要传递一个参数就够了。
  • 如果赋值后t2 = t1. 案例说应该传递2个参数。但是还是原来的调用方法,只能传递一个参数。是不安全的,所以会报错
返回值兼容性
type IFn5 = (name: string) => string
type IFn6 = (name: string) => string | number

let t5!: IFn5
let t6!: IFn6

t5 = t6 // error
t6 = t5 // success
  • 上述实例中赋值t5 = t6是错误的。原理上跟联合类型的兼容性原理一致。
  • 通过函数参数/ 返回值得到 => 传父逆,返协子 => 参数传递更少的,返回值返回更大的

infer

infer就是进行类型推导,推导指定位置的类型

ReturnType

const fns = (a: string, b: number) => {
  return {
    a,
    b,
    name: '1'
  }
}

type ReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : never

type m1 = ReturnType<typeof fns>

Parameters

const fns = (a: string, b: number) => {
  return {
    a,
    b,
    name: '1'
  }
}

type Parameters<T extends (...args: any[]) => any> = T extends (...args: infer P) => any ? P : never
type m2 = Parameters<typeof fns>

命名空间

  • 类和命名空间 可以进行合并
// 同名的命名空间 以及类可以进行属性合并
class Person {}
namespace Person {
  export var action: string
}
const p = new Person
Person.action
  • 命名空间 可以跟函数一起合并
function User() {

}
namespace User {
  export const name = '1'
}
User.name
  • 命名空间 可以和枚举类型进行合并
enum IAction {
  add = 'add'
}
namespace IAction {
  export const del = 'del'
}
IAction.del

declare全局定义

global.d.ts

declare interface Window {
  store: string
}

declare module 'jquery' {
  function $(): {
    css(keyName: string): string
    width(): number
  }

  export default $
}

declare function $(): {
  css(keyName: string): string
  width(): number
}

declare namespace $ {
  export namespace fn {
    function extend() {

    }
  }
}
  • 可以通过declare Window 来给Window添加属性
  • 可以声明一个模块jquery

下列是通过declare 实现类的描述

// 利用class 来描述类的实例
class SqlParse {
  public db: Connection | null
  constructor(host: string, user: string, password: string, database: string)
  constructor(host: IConnectConfigOptions, user?: string, password?: string, database?: string)

  open(): Promise<Connection>
  release(): void
  query(sql: string): Promise<any>
  size(tableName: string, where?: IRecords): Promise<number>
  findOne(fields: IFieldOptions[], tableName: string, where: IRecords): Promise<IRecords>
  find(fields: IFieldOptions[], tableName: string, where?: IRecords): Promise<IRecords[]>
  find(fields: IFindOptions): Promise<IRecords[]>
  insert(fields: IRecords, tableName: string): Promise<number>
  update(fields: IRecords, tableName: string, where: IRecords): Promise<number>
  on(keyName: string, fn: IFn): void
  once(keyName: string, fn: IFn): void
  emit(keyName: string, ...args: any[]): void
  off(keyName: string, fn: IFn & { l?: IFn }): void
}

// 用来描述构造函数 以及constructor重载
interface MysqlParse {
  new (host: string, user: string, password: string, database: string): SqlParse
  new (host: IConnectConfigOptions, user?: string, password?: string, database?: string): SqlParse
}

declare const SqlParser: MysqlParse

本次分享就是这么多了,之后会不同完善文章,让文章变得更加充实