TypeScript学习小结

134 阅读13分钟

我正在参加「掘金·启航计划」

TypeScript官网:www.typescriptlang.org/

介绍

全局安装

npm install -g typescript

查看验证版本号

tsc -v

编译,然后可以得到一个同名的 js 文件

tsc helloworld.ts

#如果不在根目录下,要加反斜杠
tsc .\src\helloworld.ts

也可以用ts-node插件直接在终端查看结果,但不会生成 js 文件

npm install -g ts-node

运行

ts-node helloworld.ts

类型

const isDone: boolean = false // 布尔
const num: number = 6 // 数字
const str: string = 'zgh' // 字符串
const obj: object = {} // 对象
const u: undefined = undefined
const n: null = null

数组类型

const list1: number[] = [1, 2, 3] // 由数字组成的数组

const list2: Array<number> = [4, 5, 6] // 数组泛型

const list3: [string, number] = ['1', 2] // 元组Tuple,表示一个已知元素数量和类型的数组

const list4: (number | string)[] = [1, 'a', 'b', 2] // 不知道元素数量,类型已知

const list5: [string, number] = ['ha', 666]
const [ha, info] = list5 // 解构赋值

对象数组的类型注解

const arr: { name: string; age: number }[] = [
  { name: 'tom', age: 18 },
  { name: 'jack', age: 19 }
]

如果有同样类型的数组,可以用 类型别名

type user = { name: string; age: number }
const arr: user[] = [
  { name: 'tom', age: 18 },
  { name: 'jack', age: 19 }
]

也可以使用

class user {
  name: string
  age: number
}

const arr: user[] = [
  { name: 'tom', age: 18 },
  { name: 'jack', age: 19 }
]

Symbol类型

const sym = Symbol()
let obj = {
  [sym]: 'zgh'
}
console.log(obj[sym]) // zgh

枚举类型

enum Direction {
  NORTH,
  SOUTH,
  EAST,
  WEST
}
let dir: Direction = Direction.NORTH
console.log(dir) // 0
let dirName: string = Direction[2]
console.log(dirName) // EAST

Any类型

即任意类型,ts 允许对 any 类型的值进行任何操作

let notSure: any
notSure.user.name // ok
notSure[0] // ok
notSure() // ok
new notSure() // ok

unknown类型

就是不知道啥类型,只能被赋值给any类型和unknown类型本身

let unk: unknown

unk.user.name // Error
unk() // Error
new unk() // Error

let value: unknown

let value1: unknown = value // OK
let value2: any = value // OK
let value3: boolean = value // Error
let value4: number = value // Error
let value5: string = value // Error
let value6: object = value // Error
let value7: any[] = value // Error
let value8: Function = value // Error

void类型

表示没有任何类型,比如当一个函数没有返回值时

function getInfo(): void {
  console.log('This is message')
}

never类型

表示永不存在的值的类型。 例如,never类型是那些总是会抛出异常、没有返回值的函数表达式、箭头函数表达式的返回值类型

function error(message: string): never {
  throw new Error(message)
}

function infiniteLoop(): never {
  while (true) {}
}

联合类型

|为标记,若希望属性为多个类型中的一个,可以使用联合类型。下面的例子表示函数参数接受一个数字类型的数组或者一个字符串

let union = function(item: number[] | string) {
  if (typeof item === 'string') {
    return 'string'
  }
  return item
}
union('sss')

类型别名

使用type定义一个类型别名

// 此处直接注解name是一个string类型
let name: string

// 此处定义一个别名age
type age = string | number
let user: age
user = 123
user = 'he'

类型别名和接口的区别

type name = string

type user1 = { name: string; age: number }

interface user2 {
  name: string
  age: number
}

类型别名可以直接注解字符串、数字等类型,而接口只能注解对象

接口

function add(param: { one: number; two: number }): number {
  return param.one + param.two
}
const total = add({ one: 1, two: 2 })

注解表示param参数是一个对象,有参数one、two且类型是number,返回值是number。也可以使用解构赋值

function add({ one, two }: { one: number; two: number }): number {
  return one + two
}

使用接口

interface Person {
  first: string
  last: string
}

function greeter(person: Person): string {
  return 'hello,' + person.first + 'and' + person.last
}

let user = { first: 'zgh', last: 'ivan' }
greeter(user)

类型检查器会检查greeter的调用,greeter 有一个参数,并要求这个对象参数有名称为firstlast、类型为string的属性, 属性的顺序不会检查

interface只支持声明对象类型,可以合并扩展

interface Person {
  first: string
}
interface Person {
  last: string
}
const user = {} as Person
console.log(user.first)
console.log(user.last)

可选属性

接口里的属性并不都是必需的

interface SquareConfig {
  color?: string
  width?: number
}

function createSquare(config: SquareConfig): { color: string; area: number } {
  let newSquare = { color: 'blue', area: 100 }
  if (config.color) {
    newSquare.color = config.color
  }
  if (config.width) {
    newSquare.area = config.width * config.width
  }
  return newSquare
}
let square = createSquare({ color: 'red' })

在接口属性名称后面加?即表示非必需

createSquare()后面的{ color: string; area: number }表示返回值类型

只读属性

一些对象属性只能在对象刚刚创建的时候修改其值,使用readonly指定只读属性

interface User {
  readonly name: string
  readonly age: number
}
let my: User = { name: 'zgh', age: 24 }
my.age = 23 // Error

数组也有只读属性,let arr: ReadonlyArray<number> = [1, 2, 3]

任意属性

当一个接口中有可选属性和只读属性时,还希望允许有其他的任意属性,可以用索引签名实现

interface User {
  name?: string
  readonly age: number
  [propName: string]: any
}
let my: User = { name: 'zgh', age: 24, height: 183 }

[propName: string]: any表示属性名称是字符串类型,值是任意类型

函数类型

接口也可以描述函数类型,给接口定义一个调用签名,包括参数列表和返回值类型

interface Fun {
  (name: string, age: number): boolean
}
let myFun: Fun = function(name: string, age: number) {
  return age > 20
}
myFun('zgh', 23)

对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配

interface Fun {
  (name: string, age: number): boolean
}
let myFun: Fun = function(n: string, a: number): boolean {
  return a > 20
}

函数的参数会逐个进行检查,要求对应位置上的参数类型是兼容的

可索引类型

可索引类型有一个索引签名,包括索引的类型和返回值类型,支持数字索引和字符串索引共两种签名

interface Arr {
  [index: number]: string
}

let ar: Arr = ['red', 'green', 'blue']
let re = ar[1]
console.log(re) // 'green'

上面表示用number类型 1 去索引Arr时会得到一个string类型 'green'

内联类型注解

如果你不想写接口,可以直接使用内联类型注解。但是在多处使用相同注解时,建议使用接口

let user: {
  name: string
  age: number
}
user = {
  name: 'zgh',
  age: 23
}

方法

接口里可以写方法,如示例里定义一个say方法,返回值类型是字符串

interface Info {
  ff: number
  gg: string
  say(): string
}
function getInfo2(res: Info): string {
  const sayInfo = res.say()
  return res.ff + res.gg
}
const user = {
  gg: 'zgh',
  ff: 666,
  say(): string {
    return 'hello world'
  }
}
getInfo2(user)

断言

类型断言

类型断言就是告诉编译器你比它更懂这个类型,让它别报错,知道自己在干啥。

有尖括号<>语法和as语法两种,类似类型转换,不进行特殊的数据检查和解构。没有运行时的影响,只在编译阶段起作用。

let str1: any = 'typescript'

let res1 = <string>str1
console.log(res1) // 'typescript'

let res2: number = (<string>str1).length
console.log(res2) // 10

let str2: any = 'typescript'
let res3: number = (str2 as string).length
console.log(res3) // 10

在 TypeScript 里使用 JSX 时,只有 as 语法断言是被允许的,尖括号会和 JSX 语法产生歧义

在 typescript 中,如下代码是会报错的,因为obj类型检测就是一个空对象,不能添加其他属性

const obj = {}
obj.name = 'zgh'
obj.age = 23

使用类型断言后可以了

interface User {
  name: string
  age: number
}

const user = {} as User
user.name = 'zgh'
user.age = 23

索引签名

索引签名可以定义对象内的属性、值的类型

interface Foo {
  [propName: string]: number
}

枚举

枚举是组织收集有关联变量的一种方式

数字枚举

enum Direction {
  NORTH,
  SOUTH,
  EAST,
  WEST
}
let dir: Direction = Direction.NORTH
console.log(dir) // 0
let dirName: string = Direction[2]
console.log(dirName) // EAST
let taoWa = Direction[Direction[Direction.SOUTH]]
console.log(taoWa) // 1

被编译成 js 后的代码

var Direction
;(function(Direction) {
  Direction[(Direction['NORTH'] = 0)] = 'NORTH'
  Direction[(Direction['SOUTH'] = 1)] = 'SOUTH'
  Direction[(Direction['EAST'] = 2)] = 'EAST'
  Direction[(Direction['WEST'] = 3)] = 'WEST'
})(Direction || (Direction = {}))
var dir = Direction.NORTH
console.log(dir) // 0
var dirName = Direction[2]
console.log(dirName) // EAST
var taoWa = Direction[Direction[Direction.SOUTH]]
console.log(taoWa) // 1

分析第三行代码,Direction["NORTH"] = 0是将Direction对象中的NORTH属性值设为 0,接着执行Direction[0] = "NORTH"

js 赋值运算符返回的是被赋予的值

let a = []
function f() {
  return (a['b'] = 0)
}
f() // 0

数字枚举默认第一个值是从 0 开始,后续依次递增 1,但是也可以改变任意枚举成员关联的数字

enum Direction {
  NORTH, // 0
  SOUTH = 3, // 3
  EAST, // 4
  WEST // 5
}

字符串枚举

枚举类型的值可以是字符串

enum Direction {
  NORTH = 'north',
  SOUTH = 'south',
  EAST = 'east',
  WEST = 'west'
}
let resData = 'east' // 假设resData就是后端返回的值
let res = resData as Direction
if (res === Direction.NORTH) {
  console.log('密码正确')
} else {
  console.log('甩锅给后端')
}

常量枚举

enum Dir {
  False,
  True,
  Undefined,
  Null
}
const r = Dir.False

编译成 js 后

var Dir
;(function(Dir) {
  Dir[(Dir['False'] = 0)] = 'False'
  Dir[(Dir['True'] = 1)] = 'True'
  Dir[(Dir['Undefined'] = 2)] = 'Undefined'
  Dir[(Dir['Null'] = 3)] = 'Null'
})(Dir || (Dir = {}))
var r = Dir.False

可以看到const r = Dir.False编译后没啥区别,还是存在变量Dir。如果使用常量枚举可以看到简化了很多

const enum Dir {
  False,
  True,
  Undefined,
  Null
}
const r = Dir.False

// 编译后的js
var r = 0 /* False */

枚举的静态方法

使用namespace可以给枚举类型添加静态方法

例子表示是否上班(不双休的就算了),注意export不可少

enum Weekday {
  Monday,
  Tuesday,
  Wednesday,
  Thursday,
  Friday,
  Saturday,
  Sunday
}

namespace Weekday {
  export function isBusinessDay(day: Weekday) {
    switch (day) {
      case Weekday.Saturday:
      case Weekday.Sunday:
        return false
      default:
        return true
    }
  }
}

const mon = Weekday.Monday
const sun = Weekday.Sunday

console.log(Weekday.isBusinessDay(mon)) // true
console.log(Weekday.isBusinessDay(sun)) // false

开放式枚举

可以拆分枚举块,在延续块中必须设置初始值,否则报错

enum Color {
  Red,
  Green,
  Blue
}

enum Color {
  DarkRed = 3,
  DarkGreen,
  DarkBlue
}

编译后的结果如下:

;(function(Color) {
  Color[(Color['Red'] = 0)] = 'Red'
  Color[(Color['Green'] = 1)] = 'Green'
  Color[(Color['Blue'] = 2)] = 'Blue'
})(Color || (Color = {}))
;(function(Color) {
  Color[(Color['DarkRed'] = 3)] = 'DarkRed'
  Color[(Color['DarkGreen'] = 4)] = 'DarkGreen'
  Color[(Color['DarkBlue'] = 5)] = 'DarkBlue'
})(Color || (Color = {}))

函数

函数声明

在没有提供函数实现的情况下,有两种声明函数的方式

type f = {
  (info: string): string
}

type g = (info: string) => string

表示有一个函数参数是info,参数和返回值都是字符串类型

参数注解

// 内联类型注解
function getInfo(info: string) {}

// 接口类型注解
interface Info {
  ff: number
  gg: string
}
function getInfo2(res: Info) {
  return res.ff + res.gg
}
getInfo2({ gg: 'zgh', ff: 666 })

// 可选参数
function getInfo3(info: string, msg?: string) {}

// 参数设置默认值
function getInfo4(info: string = 'success') {}

返回值注解

通常不需要给函数返回值添加注解,编译器会推断出来

// 没有返回值
function getInfo(): void {
  console.log('This is message')
}

// 返回{name: string, age: number}
function getUser(info: string): { name: string; age: number } {
  return { name: info, age: 23 }
}
getUser('zgh')

函数重载

函数根据传入不同的参数而返回不同类型的数据

function f(x: string): string
function f(x: number): number
function f(x) {
  if (typeof x === 'string') {
    return x
  } else if (typeof x === 'number') {
    return x + 1
  }
}

console.log(f(1))

这样改变后,重载的 f 函数在调用的时候会进行正确的类型检查。

编译器会先从重载列表的第一个重载定义开始查找匹配项,直到选择出正确的检查类型。 因此,在定义重载的时候,一定要把最精确的定义放在最前面。

注意,function f(x) {}并不是重载列表的一部分,这里只有两个,以其它参数调用 f 会报错。

基本使用

typescript 中的类和 ES6 中的类大部分相同

class Foo {
  content: string = 'hello world'
  say() {
    return this.content
  }
}

class Bar extends Foo {
  song() {
    return 'dadada'
  }
}

const msg = new Bar()
console.log(msg.say()) // hello world
console.log(msg.song()) // dadada

首先声明了一个 Foo 类,有 content 属性和 say 方法。然后声明了一个 Bar 类继承 Foo 类,创建实例后调用 say 和 song 方法均可行。

类的重写,就是子类可以重新编写父类里边的代码。比如在子类 Bar 中也写一个 say 方法,可以返回其他内容。 super关键字可以调用父类中的方法

class Bar extends Foo {
  song() {
    return 'dadada'
  }
  say() {
    return super.say() + ' 666'
  }
}

const msg = new Bar()
console.log(msg.say()) // hello world 666

类的访问类型

类的访问类型有publicprivateprotected三种,分别表示公共的、私有的、受保护的。

public类型在类的内部和外部都可访问,默认的类型就是public

private类型只能在类的内部访问,外部和继承的子类都不能访问

protected类型能在类的内部和继承的子类中访问,不能在外部访问

class Foo {
  private con: number = 8
  protected content: string = 'hello world'
  public say() {
    return this.content
  }
}

class Bar extends Foo {
  song() {
    return this.content
  }
}

const msg = new Bar()
msg.say()

类的构造函数

在类 Foo 中定义一个未赋初值的 name 属性、一个赋初值的 age 属性,在创建实例时传递参数通过构造器给 name 属性赋值

class Foo {
  name: string
  age: number = 23
  constructor(name) {
    this.name = name
  }
  say() {
    return this.age
  }
}
const res = new Foo('zgh')
console.log(res) // Foo { name: 'zgh', age: 23 }
console.log(res.say()) // 23

以上写法便于理解,但是还有简便写法

class Foo {
  constructor(public name: string, public age: number) {}
  say() {
    return this.name
  }
}
const res = new Foo('zgh', 23)
console.log(res) // Foo { name: 'zgh', age: 23 }

这种写法相当于定义了 name 和 age 属性,然后在构造器中进行赋值

注意属性前面的public关键字不能少,否则报错Property 'name' does not exist on type 'Foo'.

在子类中使用构造器需要使用super()调用父类的构造器,如果有参数还需传递参数。 如果在父类中没有显示的声明构造器,也有默认的构造器constructor() {},仍需调用super()

class Foo {
  constructor(public name: string, public age: number) {}
  say() {
    return this.name
  }
}

class Bar extends Foo {
  constructor(public name: string, public age: number, public msg: string) {
    super(name, age)
  }
}

const res = new Bar('zgh', 23, 'handsome')
console.log(res) // Bar { name: 'zgh', age: 23, msg: 'handsome' }

类的 getter、setter

假设有一个私有属性_age,外部是不能访问的,可以通过getter、setter属性从外部获取和设置

class girlFriend {
  constructor(private _age: number) {}
  get age() {
    return this._age
  }
  set age(age: number) {
    this._age = age
  }
}
const girl = new girlFriend(18)
console.log(girl.age) // 18
girl.age = 20
console.log(girl.age) // 20

抽象类

抽象类和抽象方法都以abstract关键字开始

abstract class Hobby {
  abstract skill(): string // 抽象方法没有具体逻辑,不能加大括号
}

class Song extends Hobby {
  skill() {
    return '唱'
  }
}

class Jump extends Hobby {
  skill() {
    return '跳'
  }
}

class Rap extends Hobby {
  skill() {
    return 'rap'
  }
}

tsconfig.json

tsconfig.json文件是用来配置如何编译 ts 文件的,一般在项目的根目录下,使用下方命令可生成

tsc --init

默认是编译所有 ts 文件

include表示包含哪些要编译的文件,exclude表示不包含哪些文件,二者都可使用通配符

files指定一个包含相对或绝对文件路径的列表

{
  "include": ["bar.ts"],
  //   "include": ["src/**/*"],
  //   "exclude": ["bar.ts"],
  //   "files": ["foo.ts", "bar.ts"],
  "compilerOptions": {}
}

文件名称只能用双引号,用单引号会报错

泛型

泛型,generic,即泛指的类型,以尖括号 <>定义,括号里面是泛型名称,一般使用<T>表示泛型

泛型在函数中的使用

先看一个联合类型的例子,函数参数可以是字符串或者数字

function foo1(first: string | number, last: string | number) {
  return `${first}-${last}`
}
foo1('hello', 'world')

使用泛型后的例子如下,定义了一个<Fan>泛型,在调用函数时要声明泛型的具体类型

function foo2<Fan>(first: Fan, last: Fan) {
  return `${first}-${last}`
}
foo2<string>('hello', 'world')

foo2<number>(1, 2)

定义一种type或者interface,可以传入泛型参数,达到类型复用的效果

type Fan<T> = {
  [key: string]: T
}

const obj: Fan<number> = {
  a: 1,
  b: 2
}

泛型在数组中的使用

有两种表示方式:Array<T>T[]

function foo3<T>(first: T[]) {
  return first.length
}
foo3<string>(['hello', 'world'])

function foo4<T>(first: Array<T>) {
  return first.length
}
foo4<string>(['hello', 'world'])

定义多个泛型

比如定义两个泛型 T、P

function foo5<T, P>(first: T, second: P) {
  return `${first}-${second}`
}
foo5<string, number>('hi', 666)

泛型在类中的使用

class Foo<T> {
  constructor(private first: T[]) {}
  say(index: number): T {
    return this.first[index]
  }
}

const gen = new Foo<string>(['hello', 'world'])
gen.say(1) // 'world'

泛型继承

上个例子中,假如传入对象数组,希望调用 say 方法返回传入的 name 值,直接改成this.first[index].name会报错 Property 'name' does not exist on type 'T'.,这时就要用到泛型的继承了

interface Person {
  name: string
}
class Foo<T extends Person> {
  constructor(private first: T[]) {}
  say(index: number): string {
    return this.first[index].name
  }
}
const fff = new Foo([{ name: 'z' }, { name: 'g' }, { name: 'h' }])
fff.say(1) // 'g'

这里省略了参数类型,因为泛型的类型推断,所以也不会报错,完整的写法如下

const fff = new Foo<{ name: string }>([{ name: 'z' }, { name: 'g' }, { name: 'h' }])

泛型约束

class Foo<T> {
  constructor(private first: T[]) {}
  say(index: number): T {
    return this.first[index]
  }
}

const gen1 = new Foo<boolean>([true, false])
const gen2 = new Foo<string>(['hello', 'world'])
const gen3 = new Foo<number>([1, 2])

上面的例子可以看到泛型可以是stringnumberboolean等类型, 现在进行泛型约束,使其类型只能是number或者string

class Foo<T extends number | string> {
  constructor(private first: T[]) {}
  say(index: number): T {
    return this.first[index]
  }
}

const gen2 = new Foo<string>(['hello', 'world'])
const gen3 = new Foo<number>([1, 2])