TS学习笔记

103 阅读10分钟

1. 安装 TypeScript

命令行运行如下命令,全局安装 TypeScript:

npm install -g typescript

安装完成后,在控制台运行如下命令,检查安装是否成功:

tsc -V 

2. 手动编译代码

编写 TS 程序

src/helloworld.ts

function greeter (person:string) {
  return 'Hello, ' + person
}

let user = 'Yee'

console.log(greeter(user))

helloworld.ts 文件,运行以下命令:

tsc helloworld.ts

持续监听文件变动 ts 自动编译成 js :

tsc --watch helloworld.ts

输出结果为一个 helloworld.js 文件。

在命令行上,通过 Node.js 运行这段代码:

node helloworld.js

控制台输出:

Hello, Yee

3. 基础类型

  • 基本类型:stringnumberbooleansymbolbigintnullundefined

  • 引用类型:arrayTuple(元组)、 object(包含Object{})、function

  • 特殊类型:anyunknowvoidnerverEnum(枚举)

  • 其他类型:类型推理字面量类型交叉类型

数组

有两种定义方法:

第一种,可以在元素类型后面接上[ ],表示由此类型元素组成的一个数组:

let list1: number[] = [1, 2, 3]

第二种方式是使用数组泛型,Array<元素类型>:

let list2: Array<number> = [1, 2, 3]

元组

允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。 比如,你可以定义一对值分别为 stringnumber 类型的元组。

let t1: [string, number]
t1 = ['hello', 10] // OK
t1 = [10, 'hello'] // Error

当访问一个已知索引的元素,会得到正确的类型:

console.log(t1[0].substring(1)) // OK
console.log(t1[1].substring(1)) // Error, 'number' 不存在 'substring' 方法

枚举(enum)

使用枚举类型可以为一组数值赋予友好的名字

enum Color {
  Red,
  Green,
  Blue
}

// 枚举数值默认从0开始依次递增
// 根据特定的名称得到对应的枚举数值
let myColor: Color = Color.Green  // 1
console.log(myColor, Color.Red, Color.Blue)  // 1 0 2

默认情况下,从 0 开始为元素编号。 也可以手动的指定成员的数值。 例如,我们将上面的例子改成从 1 开始编号:

enum Color {Red = 1, Green, Blue}
let c: Color = Color.Green  // 2

或者,全部都采用手动赋值:

enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Blue   // 4

枚举类型提供的一个便利是你可以由枚举的值得到它的名字。 例如,我们知道数值为 2,但是不确定它映射到 Color 里的哪个名字,我们可以查找相应的名字:

enum Color {Red = 1, Green, Blue}
let colorName: string = Color[2]

console.log(colorName)  // 'Green'

4. 联合类型、类型断言、类型推断

联合类型

表示取值可以为多种类型中的一种

需求1: 定义一个函数得到一个数字或字符串值的字符串形式值

function toString2(x: number | string) : string {
  return x.toString()
}

类型断言

通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。 类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。 它没有运行时的影响,只是在编译阶段起作用。 TypeScript 会假设你,程序员,已经进行了必须的检查。

类型断言有两种形式。 其一是“尖括号”语法, 另一个为 as 语法

/* 
类型断言(Type Assertion): 可以用来手动指定一个值的类型
语法:
    方式一: <类型>值
    方式二: 值 as 类型  tsx中只能用这种方式
*/

/* 需求: 定义一个函数得到一个字符串或者数值数据的长度 */
function getLength(x: number | string) {
  if ((<string>x).length) {
    return (x as string).length
  } else {
    return x.toString().length
  }
}
console.log(getLength('abcd'), getLength(1234))

类型推断

类型推断: TS会在没有明确的指定类型的时候推测出一个类型
有下面2种情况: 1. 定义变量时赋值了, 推断为对应的类型. 2. 定义变量时没有赋值, 推断为any类型

/* 定义变量时赋值了, 推断为对应的类型 */
let b9 = 123 // number
// b9 = 'abc' // error

/* 定义变量时没有赋值, 推断为any类型 */
let b10  // any类型
b10 = 123
b10 = 'abc'

5. 函数

// 命名函数
function add(x, y) {
  return x + y
}

// 匿名函数
let myAdd = function(x, y) { 
  return x + y;
}

// 完整写法
let myAdd2: (x: number, y: number) => number = 
function(x: number, y: number): number {
  return x + y
}

可选参数、默认参数、剩余参数、函数重载

/* 
函数重载: 函数名相同, 而形参不同的多个函数
需求: 我们有一个add函数,它可以接收2个string类型的参数进行拼接,也可以接收2个number类型的参数进行相加 
*/

// 重载函数声明
function add (x: string, y: string): string
function add (x: number, y: number): number

// 定义函数实现
function add(x: string | number, y: string | number): string | number {
  // 在实现上我们要注意严格判断两个参数的类型是否相等,而不能简单的写一个 x + y
  if (typeof x === 'string' && typeof y === 'string') {
    return x + y
  } else if (typeof x === 'number' && typeof y === 'number') {
    return x + y
  }
}

console.log(add(1, 2))
console.log(add('a', 'b'))
// console.log(add(1, 'a')) // error

6. 接口

TypeScript 的核心原则之一是对值所具有的结构进行类型检查。使用接口(Interfaces)来定义对象的类型。接口是对象的状态(属性)和行为(方法)的抽象(描述)

readonly 只读属性

?可选属性,可以对可能存在的属性进行预定义

interface IPerson {
  readonly id: number
  name: string
  age: number
  sex?: string
}

函数类型

接口能够描述 JavaScript 中对象拥有的各种各样的外形。 除了描述带有属性的普通对象外,接口也可以描述函数类型。

为了使用接口表示函数类型,我们需要给接口定义一个调用签名。它就像是一个只有参数列表和返回值类型的函数定义。参数列表里的每个参数都需要名字和类型。

/* 
接口可以描述函数类型(参数的类型与返回的类型)
*/

interface SearchFunc {
  (source: string, subString: string): boolean
}

这样定义后,我们可以像使用其它接口一样使用这个函数类型的接口。 下例展示了如何创建一个函数类型的变量,并将一个同类型的函数赋值给这个变量。

const mySearch: SearchFunc = function (source: string, sub: string): boolean {
  return source.search(sub) > -1
}

console.log(mySearch('abcd', 'bc'))

7. 类

/* 
类的基本定义与使用
*/
 // 定义一个类
class Greeter {
  // 声明属性
  message: string

  // 构造方法
  constructor (message: string) {
      // 初始化值
    this.message = message
  }

  // 一般方法
  greet (): string {
    return 'Hello ' + this.message
  }
}

// 创建类的实例化对象
const greeter = new Greeter('world')
// 调用实例的方法
console.log(greeter.greet())  // Helloworld

继承

/* 
类的继承
*/
class Animal {
  run (distance: number) {
    console.log(`Animal run ${distance}m`)
  }
}

class Dog extends Animal {
  cry () {
    console.log('wang! wang!')
  }
}

const dog = new Dog()
dog.cry() 
dog.run(100) // 可以调用从父中继承得到的方法

这个例子展示了最基本的继承:类从基类中继承了属性和方法。 这里,Dog 是一个 派生类 ,它派生自 Animal 基类,通过 extends 关键字。 派生类通常被称作子类,基类通常被称作超类

因为 Dog 继承了 Animal 的功能,因此我们可以创建一个 Dog 的实例,它能够 cry()run()

子类继承父类后子类的实例就拥有了父类中的属性和方法,可以增强代码的可复用性

将子类公用的方法抽象出来放在父类中,自己的特殊逻辑放在子类中重写父类的逻辑

super 可以调用父类上的方法和属性

修饰符

public,默认,可以自由访问

private ,不能在声明它的类的外部访问

protected,类内部和子类可以访问

readonly,将属性设置为只读,只读属性必须在声明时或构造函数里被初始化。

存取器

TypeScript 支持通过 getters/setters 来截取对对象成员的访问。 它能帮助你有效的控制对对象成员的访问。

下面来看如何把一个简单的类改写成使用 getset。 首先,我们从一个没有使用存取器的例子开始。

class Person {
  firstName: string = 'A'
  lastName: string = 'B'
  get fullName () {
    return this.firstName + '-' + this.lastName
  }
  set fullName (value) {
    const names = value.split('-')
    this.firstName = names[0]
    this.lastName = names[1]
  }
}

const p = new Person()
console.log(p.fullName) // A-B

p.firstName = 'C'
p.lastName =  'D'
console.log(p.fullName)  // C-D

p.fullName = 'E-F'
console.log(p.firstName, p.lastName)   // E  F

静态属性

静态属性存在于类本身上面而不是类的实例上。

/* 
静态属性, 是类对象的属性
非静态属性, 是类的实例对象的属性
*/

class Person {
  name1: string = 'A'
  static name2: string = 'B'
}

console.log(Person.name2)  // 此处直接访问Person类的静态属性  B
console.log(new Person().name1) // A

抽象类

抽象类做为其它派生类的基类使用。 它们不能被实例化。不同于接口,抽象类可以包含成员的实现细节。 abstract 关键字是用于定义抽象类和在抽象类内部定义抽象方法。

/* 
抽象类
  不能创建实例对象, 只有实现类才能创建实例
  可以包含未实现的抽象方法
*/

abstract class Animal {

  abstract cry ()

  run () {
    console.log('run()')
  }
}

class Dog extends Animal {
  cry () {
    console.log(' Dog cry()')
  }
}

const dog = new Dog()
dog.cry()   // Dog cry()
dog.run()   // run()

类实现接口

与 C# 或 Java 里接口的基本作用一样,TypeScript 也能够用它来明确的强制一个类去符合某种契约。

/* 
类类型: 实现接口
1. 一个类可以实现多个接口
2. 一个接口可以继承多个接口
*/

interface Alarm {
  alert(): any;
}

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

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

一个类可以实现多个接口

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

接口继承接口

和类一样,接口也可以相互继承。 这让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里。

interface LightableAlarm extends Alarm, Light {

}

接口和接口之间叫继承(extends),类和接口之间叫实现(implements)

8. 泛型

指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定具体类型的一种特性。

语法

泛型的语法是 <> 里写类型参数,一般可以用 T 来表示。

function print<T>(arg:T):T {
    console.log(arg)
    return arg
}

多个泛型参数的函数

一个函数可以定义多个泛型参数

function swap <K, V> (a: K, b: V): [K, V] {
  return [a, b]
}
const result = swap<string, number>('abc', 123)
console.log(result[0].length, result[1].toFixed())

泛型接口

interface IbaseCRUD <T> {
  data: T[]
  add: (t: T) => void
  getById: (id: number) => T
}

class User {
  id?: number; //id主键自增
  name: string; //姓名
  age: number; //年龄

  constructor (name, age) {
    this.name = name
    this.age = age
  }
}

class UserCRUD implements IbaseCRUD <User> {
  data: User[] = []
  
  add(user: User): void {
    user = {...user, id: Date.now()}
    this.data.push(user)
    console.log('保存user', user.id)
  }

  getById(id: number): User {
    return this.data.find(item => item.id===id)
  }
}


const userCRUD = new UserCRUD()
userCRUD.add(new User('tom', 12))
userCRUD.add(new User('tom2', 13))
console.log(userCRUD.data)

泛型类

在定义类时, 为类中的属性或方法定义泛型类型 在创建类的实例时, 再指定特定的泛型类型

class GenericNumber<T> {
  zeroValue: T
  add: (x: T, y: T) => T
}

let myGenericNumber = new GenericNumber<number>()
myGenericNumber.zeroValue = 0
myGenericNumber.add = function(x, y) {
  return x + y 
}

let myGenericString = new GenericNumber<string>()
myGenericString.zeroValue = 'abc'
myGenericString.add = function(x, y) { 
  return x + y
}

console.log(myGenericString.add(myGenericString.zeroValue, 'test'))
console.log(myGenericNumber.add(myGenericNumber.zeroValue, 12))

泛型约束

如果我们直接对一个泛型参数取 length 属性, 会报错, 因为这个泛型根本就不知道它有这个属性

// 没有泛型约束
function fn <T>(x: T): void {
  // console.log(x.length)  // error
}

我们可以使用泛型约束来实现

interface Lengthwise {
  length: number;
}

// 指定泛型约束
function fn2 <T extends Lengthwise>(x: T): void {
  console.log(x.length)
}

我们需要传入符合约束类型的值,必须包含必须 length 属性:

fn2('abc')
// fn2(123) // error  number没有length属性

9. 其他

声明文件:

一般声明文件都会单独写成一个 xxx.d.ts 文件

declare var jQuery: (selector: string) => any;

jQuery('#foo');

很多的第三方库都定义了对应的声明文件库, 库文件名一般为 @types/xxx, 可以在 https://www.npmjs.com/package/package 进行搜索

有的第三库在下载时就会自动下载对应的声明文件库(比如: webpack),有的可能需要单独下载(比如jQuery/react)