typescript 基础语法学习

717 阅读8分钟

简介

关于为什么要学习ts, ts相对于 js 有什么好处, 我在这篇文章有简要介绍, 接下里我们将学习ts的基本使用, 学习其中的语法, 如果学习过 像java 这种强类型的语言,那么学ts会很轻松.

上手

首先安装, 初始化 tsconfig.json

// 安装 ts
yarn add typescript --dev
// 生成 tsconfig.json配置文件
yarn tsc --init

image.png 在配置文件中有许多配置选项,简单介绍个重要的属性

  1. target: ts要编译成的版本, 默认是 'es5', 如果改成 'es2015',那么 es6 就不会被编译了.
  2. module: 输出的代码将会按照什么方式去模块化, 默认是 'commonjs',那么默认导入导出就是 require和 module.exports 的方式
  3. outDir: ts文件编译后的文件夹, 一般是dist目录
  4. rootDir: 需要编译的文件夹, 一般是 src
  5. sourceMap: boolean类型值, 可以查找ts内部文件
  6. strict: boolean类型, 开启会对类型检查非常严格, 每个值都需要声明类型

ts 原始数据类型

// 原始数据类型

const a: string = 'foobar'

const b: number = 100 // NaN Infinity

const c: boolean = true // false

// 在非严格模式(strictNullChecks)下,
// string, number, boolean 都可以为空
// const d: string = null
// const d: number = null
// const d: boolean = null

const e: void = undefined

const f: null = null

const g: undefined = undefined

还有一个 原始类型比较特殊, Symbol,这是在 es2015 新增的特性, 但是ts 默认编译成的是 es5, 所以要使用 Symbol, tsconfig.json中的 lib 选项必须包含 ES2015

object类型

object 类型是指除了原始类型以外的其它类型, 像数组, 方法,对象都属于 object

const foo: object = function () {} // [] // {}

// 如果需要明确限制对象类型,则应该使用这种类型对象字面量的语法,或者是「接口」
const obj: { foo: number, bar: string } = { foo: 123, bar: 'string' }

数组类型 与 元组类型

// 数组类型的两种表示方式
const arr1: Array<number> = [1, 2, 3]
const arr2: number[] = [1, 2, 3]
// 元组类型
const tuple: [number, string] = [18, 'zce']
// 可以通过解构的方式拿到值
const [age, name] = tuple

枚举类型

// 标准的数字枚举
enum PostStatus {
  Draft = 0,
  Unpublished = 1,
  Published = 2
}

// 数字枚举,枚举值自动基于前一个值自增
enum PostStatus1 {
  Draft = 6,
  Unpublished, // => 7
  Published // => 8
}

// 字符串枚举
enum PostStatus2 {
  Draft = 'aaa',
  Unpublished = 'bbb',
  Published = 'ccc'
}

const enum PostStatus3 {
  Draft, // 默认从 0开始
  Unpublished,
  Published
}

const post = {
  status: PostStatus3.Draft // 使用方法
}

数组类型

数组类型主要是为了声明参数类型

// 可以赋默认值, 可以 以...形式获取剩余参数
function func1 (a: number, b: number = 10, ...rest: number[]): string {
  return 'func1'
}

func1(100, 200)
func1(100)
func1(100, 200, 300)

any类型

any类型是弱类型, 主要是为了兼容以前的老项目, 不建议使用

隐式类型推断

注意下面两段代码的区别,前面一个会报错因为 age被隐式推断为number类型, 后面的不会报错因为 age被隐式推断为any类型

let age = 18. // ts 推断 age 为 number 类型
age = 'string' // 会报错, 因为 age是 number类型

let age // ts 推断 age 为any类型
age = 18 // any 类型不会报错
age = 'string' // any 类型不会报错

断言

在一些特殊的情况下, ts 是无法推断出一些变量的具体类型,请看下面的例子

截屏2021-10-20 下午9.19.12.png

res * res 为什么会报错呢?

截屏2021-10-20 下午9.19.57.png

看到报错提示就应该明白了, res 有可能是 underfined, 那这个时候我们该怎么办呢? 可以用 ts的断言

截屏2021-10-20 下午9.21.22.png

通过 as 关键字 告诉ts我们确定res就是number,我们还有另外一种方式断言

const num2 = <number>res

通过在 res 前面加尖括号方式断言, 这种方式在 react jsx语法中因为冲突原因不能使用, 所以建议使用第一种方式.

接口

接口最直观的体现就是约定一个对象只能有哪些成员,而且这些成员的类型也固定了. 例如:

// 定义一个接口
interface Post {
  title: string
  content: string
}
// 传入的参数对象必须按照接口来
function printPost (post: Post) {
  console.log(post.title)
  console.log(post.content)
}

printPost({
  title: 'Hello TypeScript',
  content: 'A javascript superset'
})

当然, 在接口中也有可选属性, 在 属性后面加一个问号就代表该属性可有可无, 还可以在接口前面加 readonly,代表该属性 只读,不能被修改. 例如下面的例子

interface Post {
  title: string
  content: string
  subtitle?: string
  readonly summary: string
}

class Person {
  name: string // 一般需要在头部声明 属性
  age: number
  
  constructor (name: string, age: number) {
  // 这里初始化, 当然在 外面声明再赋值也是可以的
    this.name = name
    this.age = age
  }

  sayHi (msg: string): void { // 这里就是对函数 约定,无返回值, 传入参数为 字符串
    console.log(`I am ${this.name}, ${msg}`)
  }
}

学过 java 的应该知道, public, protected, private的作用是什么

class Person {
  public name: string // = 'init name'
  private age: number
  protected gender: boolean
  
  constructor (name: string, age: number) {
    this.name = name
    this.age = age
    this.gender = true
  }
}

就比如上面的类, 如果没有声明上面三者其一, 就会默认声明 为 public, 这种属性在我们实例化之后是可以直接拿到的, 但是 private 在实例化之后是拿不到了, 也就是私有属性. protected是只能在继承的 类里面拿到, 比如还有一个 Student 类继承了 Person 类, 那么 在 Student内部可以拿到 gender, 实例化的 Student也同样是拿不到的.

类和接口

相比于类, 接口是比较抽象的, 我们首先来看一个例子

class Person {
  eat (food: string): void {
    console.log(`优雅的进餐: ${food}`)
  }

  run (distance: number) {
    console.log(`直立行走: ${distance}`)
  }
}

class Animal {
  eat (food: string): void {
    console.log(`呼噜呼噜的吃: ${food}`)
  }

  run (distance: number) {
    console.log(`爬行: ${distance}`)
  }
}

上面有两个类, 两个类都有同样的两个方法,那么我们就可以实现一个接口来约束这两个类必须要有 eat 和 run 方法

interface EatAndRun {
  eat(food: string): void;
  run(distance: number): void;
}
class Person implements EatAndRun {
  eat(food: string): void {
    console.log(`优雅的进餐: ${food}`);
  }

  run(distance: number) {
    console.log(`直立行走: ${distance}`);
  }
}
class Animal implements EatAndRun {
  eat(food: string): void {
    console.log(`呼噜呼噜的吃: ${food}`);
  }

  run(distance: number) {
    console.log(`爬行: ${distance}`);
  }
}

通过 implements 关键字 实现 EatAndRun 接口 ,如果在类里面有个方法没有实现就会报错. 但是 一般来说一个接口实现一个功能就行, 如果Person 不想实现run方法怎么办呢? 我们可以这么做

interface Eat {
  eat (food: string): void
}

interface Run {
  run (distance: number): void
}

class Person implements Eat, Run {
  eat (food: string): void {
    console.log(`优雅的进餐: ${food}`)
  }

  run (distance: number) {
    console.log(`直立行走: ${distance}`)
  }
}

类是可以通过 逗号实现多个 接口的, 所以如果只需要实现一个那就 implements 一个.

抽象类

抽象类与接口比较类似, 有一个区别就是 抽象类 可以具体实现某个方法

abstract class Animal {
  eat (food: string): void {
    console.log(`巴拉巴拉: ${food}`)
  }

  abstract run (distance: number): void
}

class Dog extends Animal {
  run(distance: number): void {
    console.log('四脚爬行', distance)
  }

}

const d = new Dog()
d.eat('啦啦啦')
d.run(100)

就比如上面的例子, eat 方法可以直接实现, 然后也可以通过 abstract 关键字定义抽象方法. 接着在Dog 类中通过 extends 关键字 继承 抽象类, 需要注意的是抽象类中的抽象方法也必须被实现.

泛型

泛型指的是在我们定义 函数、接口或者类的时候没有指定具体的类型,等到使用的时候才去指定具体的类型,泛型的作用就是极大程度的复用代码.
以函数为例,

function createNumberArray (length: number, value: number): number[] {
  const arr = Array<number>(length).fill(value)
  return arr
}

function createStringArray (length: number, value: string): string[] {
  const arr = Array<string>(length).fill(value)
  return arr
}

上面两个函数, 一个是返回数字类型的数组, 一个是返回 字符串类型的数组, 可以看到两个函数长得几乎一样, 那么有什么方法可以去复用呢?

function createArray<T> (length: number, value: T): T[] {
  const arr = Array<T>(length).fill(value)
  return arr
}

const res = createArray<string>(3, 'foo')

我们通过在函数名后面加尖括号的方式自定义一个类型T, 然后value声明为T, 返回值为 T类型的数组,在我们调用的时候 就可以按照自己的需求传递一个类型达到复用的目的.

类型声明

在我们开发的过程中,一般都会用到第三方的 npm 模块,而这么模块并不一定是通过 typescript 编写的,所以说它提供的成员就不会有强类型的体验,比如loadash包

image.png

我们安装了一个lodash库, 使用里面的一个camelCase方法, 我们知道 它返回的是一个驼峰格式的字符串,传入的是一个字符串,可是我们看到并没有提示, 这个时候我们需要声明这个方法

image.png

声明之后我们发现就有提示了, 这种 decalre 其实就是为了兼容普通的js模块. 由于现在 ts社区是比较强大的,一些常用的 npm模块都已经提供了对应的声明, 我们只需要安装对应的类型声明模块就可以了. 比如loadsh, 对应的声明模块 是 @types/lodash 安装过后就有智能提示了

image.png

当然,现在也有很多 npm模块内置了 声明模块,不需要我们再安装额外的声明模块