TypeScript

214 阅读2分钟

TypeScript是一门基于JavaScript之上的编程语言,解决了JavaScript类型系统的问题。TypeScript大大提高代码的可靠程度。TypeScript可以完全按照JavaScript标准语法编写代码。

类型系统

JavaScript是一门弱类型且是动态类型的语言,缺失了类型系统的可靠性。

强类型和弱类型

类型安全区分,编程语言分为强类型和弱类型。

强类型:语言层面限制函数的实参类型必须与形参类型相同。

弱类型:语言层面不会限制实参的类型。

强类型语言中不允许任意的隐式类型转换,而弱类型语言则允许任意的隐式类型转换。

变量类型允许随时改变的特点,不是强弱类型的差异

静态类型和动态类型

类型检查区分,编程语言分为静态类型和动态类型。

静态类型:一个变量声明时它的类型就是明确的,声明过后,它的类型就不允许再修改。

动态类型:运行阶段才能明确变量的类型,而且变量的类型随时可以改变。

动态类型语言中的变量没有类型,变量中存放的值是有类型的。

TypeScript快速上手

安装

采用yarn进行安装

yarn add typescript --dev

使用

新建一个1.ts文件

const hello = (name: any) =>  {
  console.log(`Hello, ${name}`)
}

hello('TypeScript')

使用yarn tsc 1.ts可以将.ts文件编译成.js文件,添加的类型注解会被移除掉

var hello = function (name) {
    console.log("Hello, " + name);
};
hello('TypeScript');

配置文件

其实tsc命令除了可以编译指定的某个ts文件,还可以编译整个项目。不过一般在编译整个项目之前会先创建TypeScript的配置文件。

通过 yarn tsc --init 命令可以创建一个tsconfig.json文件。

此时不能再使用tsc命令编译某个指定的ts文件了,只有编译整个项目时tsc命令才会生效,而且编译的项目中的所有ts文件需要放在src目录下。

TypeScript原始数据类型

// 原始数据类型
const a: string = 'foobar'
const b: number = 100 // NaN Infinity
const c: boolean = true // false

// 在非严格模式(strictNullChecks)下,string, number, boolean 都可以为空
// 严格模式下string, number, boolean都不能为空,可以通过设置tsconfig.json文件中的"strict": true开启严格模式
const d: string = null
const d: number = null
const d: boolean = null

// 空值,一般在函数没有返回值时用于标记返回值类型,只有null和undefined两个值,严格模式下只能是undefined
const e: void = undefined 

const f: null = null
const g: undefined = undefined

// Symbol 是 ES2015 标准中定义的成员,
// 使用它的前提是必须确保有对应的 ES2015 标准库引用
// 也就是 tsconfig.json 中的 lib 选项必须包含 ES2015
const h: symbol = Symbol()

作用域问题

// 作用域问题

// 默认文件中的成员会作为全局成员
// 多个文件中有相同成员就会出现冲突
const a = 123

// 解决办法1:立即执行函数,提供独立作用域
(function () {
    const a = 123
})()

// 解决办法2: 在当前文件使用 export,也就是把当前文件变成一个模块
// 模块有单独的作用域
const a = 123
export {}

Object类型

TypeScript中的Object不单指普通的对象类型,而泛指所有的非原始类型,也就是Object、Array、Function。

// Object 类型

export {} // 确保跟其它示例没有成员冲突

// object 类型是指除了原始类型以外的其它类型
const foo: object = function () {} // [] // {}

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

数组类型

// 数组类型

export {} // 确保跟其它示例没有成员冲突

// 数组类型的两种表示方式
const arr1: Array<number> = [1, 2, 3]
const arr2: number[] = [1, 2, 3]

// 案例 -----------------------
// 如果是 JS,需要判断是不是每个成员都是数字
// 使用 TS,类型有保障,不用添加类型判断,可以添加类型注解
function sum (...args: number[]) {
  return args.reduce((prev, current) => prev + current, 0)
}
sum(1, 2, 3) // => 6

元组类型

元组就是一个明确元素数量以及每个元素类型的数组,各个元素的类型不必完全相同。

// 元组(Tuple)

export {} // 确保跟其它示例没有成员冲突

const tuple: [number, string] = [18, 'zce']

//可以使用数组下标的方式访问某个元素
const age = tuple[0]
const name = tuple[1]
//或者使用数据结构的方式提取每个元素
const [age, name] = tuple


const entries: [string, number][] = Object.entries({
  foo: 123,
  bar: 456
})

const [key, value] = entries[0]
// key => foo, value => 123

枚举类型

枚举类型有两个特点:

  1. 可以给异步数值去分别起上一个易于理解的名字
  2. 一个枚举中只会存在几个固定的值,并不会出现超出范围的可能性

JavaScript中不存在枚举值。

// 枚举(Enum)

export {} // 确保跟其它示例没有成员冲突

// 用对象模拟枚举
const PostStatus = {
    Draft: 0,
    Unpublished: 1,
    Published: 2
}

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

// 数字枚举,可以不指定具体的枚举的值,默认会从0开始累加;若是指定了第一个枚举值,枚举值会自动基于前一个值自增
enum PostStatus {
    Draft = 6,
    Unpublished, // => 7
    Published // => 8
}

// 字符串枚举,需要手动给每个成员初始化一个明确的字符串的值
enum PostStatus {
    Draft = 'aaa',
    Unpublished = 'bbb',
    Published = 'ccc'
}


// 枚举类型会入侵到运行时的代码,会影响编译后的结果。
// 大多数类型在经过编译转换过后,最终都会被移除掉,因为它只是为了在编译过程中能进行类型检查
// 而枚举最终会被编译成一个双向的键值对对象
// 双向的键值对对象就是可以通过键去获取值,也可以通过值去获取键
// 这样做的目的就是为了让我们可以动态的根据枚举值去获取枚举的名称
enum PostStatus {
    Draft = 'aaa',
    Unpublished = 'bbb',
    Published = 'ccc'
}
PostStatus[0] // => Draft

// 常量枚举,不会侵入编译结果
const enum PostStatus {
  Draft,
  Unpublished,
  Published
}

函数类型

// 函数类型

export {} // 确保跟其它示例没有成员冲突

//函数参数对应的类型限制

// 调用函数时参数个数必须完全相同,就是形参个数等于实参个数
// 如果需要某个参数是可选的,可以使用可选参数(就是在参数后面加一个?),或者使用参数默认值。
// 这个可选的参数必须位于最后
function func1 (a: number, b: number = 10, ...rest: number[]): string {
  return 'func1'
}
func1(100, 200)
func1(100, 200, 300)

// 函数表达式对应的类型限制
const func2: (a: number, b: number) => string = function (a: number, b: number): string {
  return 'func2'
}

任意类型

// 任意类型(弱类型)

export {} // 确保跟其它示例没有成员冲突

function stringify (value: any) {
  return JSON.stringify(value)
}

stringify('string')

stringify(100)

stringify(true)

let foo: any = 'string'

foo = 100

foo.bar()

// any 类型是不安全的

隐式类型推断

在TypeScript中如果没有明确通过类型注解去标注一个变量的类型,那TypeScript会根据变量的使用情况去推断变量的类型,这种特性叫做隐式类型推断。

// 隐式类型推断

export {} // 确保跟其它示例没有成员冲突

let age = 18 // number
age = 'string'// 此时会报错,因为TypeScript已经把age推断为了number类型


// 如果TypeScript无法推断这个变量是什么类型,就会把这个变量标记为any类型
let foo
foo = 100
foo = 'string'

// 建议为每个变量添加明确的类型标注

类型断言

类型断言可以辅助TypeScript去更加明确代码中每个成员的类型。

类型断言不是类型转换,并不是把一个类型转换为另一个类型。类型转换是在代码运行时的概念,而类型断言是在编译过程中的概念,代码编译过后这个断言也就不存在了。

// 类型断言

export {} // 确保跟其它示例没有成员冲突

// 假定这个 nums 来自一个明确的接口
const nums = [110, 120, 119, 112]

const res = nums.find(i => i > 0)

//类型断言方式有两种,as 和 <类型>
const num1 = res as number
const num2 = <number>res // JSX 下不能使用

接口

接口是一种抽象的概念,可以用来约定对象的结构,我们在使用一个接口就必须遵守这个接口全部的约定。

// 接口

export {} // 确保跟其它示例没有成员冲突

//约定对象中具体应该有哪些对象成员,这些成员的类型又是什么样的
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'
})


// 可选成员、只读成员、动态成员

export {} // 确保跟其它示例没有成员冲突

interface Post {
  title: string
  content: string
  subtitle?: string // 用?标记可选成员,表示这个参数可有可无
  readonly summary: string // 用readonly标记只读成员,在初始化后不能再改变
}

const hello: Post = {
  title: 'Hello TypeScript',
  content: 'A javascript superset',
  summary: 'A javascript'
}


interface Cache {
  [prop: string]: string // 动态成员
}

const cache: Cache = {}

cache.foo = 'value1'
cache.bar = 'value2'

类可以用来描述一类具体事物的抽象特征。

// 类(Class)

export {} // 确保跟其它示例没有成员冲突

class Person {
  // 字段
  public name: string
  private age: number // 私有属性,外部访问不到,只能在此类中访问,不允许继承
  protected readonly gender: boolean // 受保护的属性,外部访问不到,允许继承。只读属性,初始化后不允许再改变值
  // 构造函数
  constructor (name: string, age: number) {
    this.name = name
    this.age = age
    this.gender = true
  }
  // 函数
  sayHi (msg: string): void {
    console.log(`I am ${this.name}, ${msg}`)
    console.log(this.age)
  }
}
class Student extends Preson{
  constructor(name: string, age: number){
    super(name, age)
    console.log(this.gender)// 输出 true
  }
}
const tom = new Person('Tom', 18)
console.log(tom.name)// 输出 Tom
console.log(tom.age)// 报错
console.log(tom.gender)// 报错



// 类与接口

export {} // 确保跟其它示例没有成员冲突
//用接口约束两个类中公共方法的类型
interface Eat {
  eat (food: string): void
}

interface Run {
  run (distance: number): void
}
// 用implements实现接口
class Person implements Eat, Run {
  eat (food: string): void {
    console.log(`优雅的进餐: ${food}`)
  }

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

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

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

抽象类可以用来约束子类中必须要有某个成员,可以包含一些具体的实现。接口不包括具体的实现。

// 抽象类

export {} // 确保跟其它示例没有成员冲突
// 用abstract修饰抽象类
abstract class Animal {
  eat (food: string): void {
    console.log(`呼噜呼噜的吃: ${food}`)
  }
  // 用abstract修饰抽象的方法
  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)

泛型

泛型是指在定义接口、函数或者类的时候没有指定具体类型,等到使用的时候再去指定类型的一种特征。

// 泛型

export {} // 确保跟其它示例没有成员冲突

// 使用泛型具体就是在函数名后面使用<>,在<>中使用泛型参数,一般用T作为名称
function createArray<T> (length: number, value: T): T[] {
  const arr = Array<T>(length).fill(value)
  return arr
}

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