Typescript分享-基础篇

259 阅读11分钟

基础篇

前置知识

官方文档

深入理解 TypeScript

学习TS的三招心法

  1. TS是JS的超集,其实就是TS是JS的语法糖而已。
  2. TS是基于JS的一门强类型的高级语言,但TS的所有语法和类型校验都只在编译环境有效。
  3. TS核心的功能就是,值的类型化。

值的类型化是什么意思

什么是值?

凡是可以被变量存储的单元都是值

它可以是简单的字符串、数字,也可以是复杂的由多条语句组成的代码单元,如类、函数、对象等

// 字符串作为值
let stringValue = 'hello world';

// 对象字面量作为值
let objectLiteralValue = {
  attribute: 'hello world'
};

// 函数作为值
let funcValue = function(){};

// 函数返回作为值
function fn(){
  return 'hello world';
}
let returnValue = fn();

// 类作为值
let classValue = class {}

所谓值的类型化,就是说每一个值都一定是某种或某几种特定类型

在TypeScript中,所有的值都具有强制类型,值的类型化是TypeScript区别于JavaScript最显著的特征。

实际上,JavaScript也是有类型的,只不过JavaScript的类型信息是在编译阶段由编译器判定,对程序员来说,值可以任意赋予,编写代码时就好像类型弱的不存在一样。

Typescript如何判断值的类型

类型注解

冒号加类型构成了类型注解,冒号前后可以包含任意空格。

// 声明字符串类型变量
let str: string;

// 声明字符串类型变量并初始化
let strValue: string = 'hello world';

// 指定类的属性类型
class Hello {
  show: boolean = true;
}

// 指定函数参数和返回值类型
function sum(a: number, b: number): number {
  return a + b;
}

类型推导

类型注解是可选的,在绝大多数未显式注解类型的情况下,编译器能自动推导出值的类型

// 变量被自动推导为字符串类型 string
let variable = 'hello world';
// 等价于
let variable: string = 'hello world';

// 返回值被自动推导为数字类型 number
function show(param: number) {
  return param;
}
// 等价于
function show(param: number): number {
  return param;
}

类型查询

类型查询是一条语句,相当于一个独立类型。

代码中任何需要显式注解类型的地方,都可以使用类型查询代替。

// 声明a为number
let a: number;

// 通过类型查询声明b的类型
let b: typeof a;
// 等价于
let b: number;

// 函数fn为函数
function fn(){}

// 通过类型查询声明d的类型为fn的类型
let d: typeof fn;
// 等价于
let d: () => void;

总结

类型注解类型推导类型查询构成了TypeScript的类型判定系统,TypeScript编译器判定值的类型时,主要是通过以上三种方式。

TS 支持的类型

// 简单类型
number
boolean
string
symbol
void    // 表示没有类型或空类型
undefined
null
never
any

// 复合类型
object  // {}
array   // 普通数组 T[]  元组 [T0, T1, T2 ....]
function // let a = function() {}; let b: typeof a;  b <==> () => void
enum
联合类型 T1 | T2
交叉类型 T1 & T2
class
interface

undefined & null

Null类型和Undefined类型的类型表现和编译选项strictNullChecks有关

// ------ strictNullChecks: true ------
// 合法
let n: null = null;
let u: undefined = undefined;
let v: void = undefined;

// 不合法
let v: void = null;

// ------ strictNullChecks: false ------
// 合法
let v1: undefined = null;
let v2: void = undefined;
let v3: null = undefined;
let x: number = null;
let y: string = undefined;

never

  1. 一个从来不会有返回值的函数,(如:如果函数内含有while(true) {}
  2. 一个总是会抛出错误的函数,(如:function foo() { throw new Error('Not Implemented') }foo的返回类型是never

这跟void区别在哪?因为function test(): void {}

  1. void是没有类型,而never是永不存在的值的类型
  2. 一个函数没有返回值时,它返回了一个void类型,一个函数根本就没有返回值时(或者总是抛出错误),它返回了一个 never
  3. void只能为他赋值undefined(strictNullChecks: true),never只能赋值为never

来自【深入理解 TypeScript】

function类型

定义:(p1: T1, p2: T2, ...) => T

// 直接定义的函数
function sum(a: number, b: number): number {
  return a + b;
}

// 通过typeof关键字获取函数sum的类型为:
// (a: number, b: number) => number;
let fn: typeof sum;

函数兼容

判断一个函数类型是否和一个函数兼容,只需判断参数类型和返回值类型是否同时兼容

// 声明fn为函数类型
let fn: (x: number, y: string) => boolean;

// 正确,参数名字不做兼容检查
fn = function(a: number, b: string): boolean {
  // ...
  return true;
}

// 错误,函数返回值类型不匹配
fn = function(a: number, b: string): string {
  // ...
  return b;
}

可选参数

// 参数a必须,参数b可选,b必须位于a之后
function test(a: number, b?: number): void {
  // ...
}

enum 枚举类型

enum T { ... },对枚举值显式初始化,那么枚举值默认为数字类型,值也可以设置成字符串类型

enum Direction {
  Up,   // 值默认为 0
  Down, // 值默认为 1
  Left, // 值默认为 2
  Right // 值默认为 3
}

// 如果添加数字默认值
enum Direction {
  Up = 2,
  Down,
  Left = 3.3
  Right
}

Direction.Up === 2;       // 显式初始化
Direction.Down === 3;     // 2 + 1 = 4.3
Direction.Left === 3.3;   // 显式初始化
Direction.Right === 4.3;  // 3.3 + 1 = 4.3

// 字符串
enum Direction {
  Up = 'UP',
  Down = 'DOWN',
  Left = 'LEFT',
  Right = 'RIGHT'
}

联合类型 & 交叉类型

联合类型: T1 | T2, 表示可能是类型T1或者T2,类似

// 类型与类型联合
let a: string | number = '3';
// 值与值联合
let b: '3' | '4' | 5 = 5
// 值与类型的联合
let c: '3' | number = '3'

交叉类型: T1 & T2,表示是类型T1T2的合集,类似

type Ta = {
  key: string
}

type Tb = {
 value: number
}

let c: Ta & Tb = {
  key: '3',
  value: 4
}

// 等价于
// type Tc = {
//   key: string
//   value: number
// }


// 不合法
let c: Ta & Tb = {
  key: '3'
}

tips

keyof关键字作用于类型,通过获取一个类型的所有属性名,生成一个新的字符串值的联合类型

interface Ia {
  key: string,
  value: number,
  name: string,
  code: number
}

let c: keyof Ia = 'code'
// 等价于
// type c = 'key' | 'value' | 'name' | 'code'

类类型

类本身就是一种类型

// 定义类
class TypeA {
  // ...
}

// 声明TypeA类型
let a: TypeA;
// 赋值TypeA类型
a = new TypeA();

Typescript里的Class 和 ES6的class

主要是区别在于属性和静态属性的区别

// ES6合法的定义
class Greeter {
  constructor(){
    // 正确,ES6中实例属性只能定义在构造器内部
    this.greeting = 'world';
  }
}
// 正确,ES6中静态属性只能定义在类外部
Greeter.greeting = 'world';

// 下面是TypeScript代码
class Greeter {
  // 定义实例属性并初始化
  greeting: string = 'world';

  // 定义静态属性并初始化
  static greeting: string = 'world';
}

访问控制 public/private/protected 修饰符

class Animal {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}

// 错误: 不能在类外访问私有成员
// error TS2341: Property 'name' is private and only accessible within class 'Animal'
new Animal("Cat").name;

consle.log(Animal.name) // Cat

protected修饰符与private修饰符的行为相似,都不能在类的外部访问。但有一点不同,protected成员可以在派生类中访问

// 定义基类
class Person {
  protected name: string;
  constructor(name: string) { this.name = name; }
}

// 定义派生类
class Employee extends Person {
  private department: string;

  constructor(name: string, department: string) {
    super(name)
    this.department = department;
  }

  public getElevatorPitch() {
    // 正确,来自父类的name成员在派生类里可以访问。虽然它位于Persion类的外部
    return `Hello, my name is ${this.name} and I work in ${this.department}.`;
  }
}

let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());

// 错误,受保护的成员 name 不能在外部访问
// error TS2445: Property 'name' is protected and only accessible within class 'Person' and its subclasses
console.log(howard.name); 

tips

class Animal {
  // private修饰符用于构造器的name参数前
  constructor(private name: string) {}
}

// 以上定义相当于
class Animal {
  private name: string;
  constructor(name: string) {
    this.name = name;
  }
}

只读修饰符 readonly

class Octopus {
  readonly name: string;
  // 在属性声明时初始化
  readonly numberOfLegs: number = 8;
  constructor (theName: string) {
    // 在构造函数里初始化
    this.name = theName;
  }
}
let dad = new Octopus("Man with the 8 strong legs");

// 错误! name 是只读的
// error TS2540: Cannot assign to 'name' because it is a constant or a read-only property
dad.name = "Man with the 3-piece suit";

tips 只读属性和常量类似,一旦初始化,之后就再也不允许被赋值。

抽象类和抽象方法

用于描述抽象类和抽象方法的关键字都是 abstract,抽象方法没有方法体

一般情况下,抽象类和抽象方法是同时出现的,但也有例外:

  1. 一个类包含抽象方法,那么这个类必须是抽象类
  2. 抽象类可以没有抽象方法
// Animal是抽象类
abstract class Animal {
  // makeSound是抽象方法,没有方法体
  abstract makeSound(): void;
  move(): void {
      console.log('roaming the earch...');
  }
}

抽象类主要是用来被继承使用,抽象方法必须在派生类中必须被实现

// Animal是抽象类
abstract class Animal {
  // makeSound是抽象方法,没有方法体
  abstract makeSound(): void;
  move(): void {
      console.log('roaming the earch...');
  }
}

// 错误,抽象方法makeSound没有实现
// error TS2515: Non-abstract class 'Dog' does not implement inherited abstract member 'makeSound' from class 'Animal'
class Dog extends Animal {
  // 空类
}

// 正确,抽象方法被实现
class Dog extends Animal {
  makeSound(): void{
    // ...
  }
}

接口类型

对象类型是TypeScript的类型系统中最复杂也是最重要的类型,对象类型主要用来描述复杂数据类型

// 声明一个值为对象字面量
let man = {name: 'joye', age: 30};

// 等价于
let man: {name: string; age: number} = {name: 'joye', age: 30};

对象类型是匿名的接口类型,对象类型没有名字,接口类型有名字。接口类型相当于为对象类型声明了一个别名

// 定义接口类型Person
interface Person {
  name: string;
  age: number;
}

// 声明变量 man 为 Person 接口类型
let man: Person = {name: 'joye', age: 30};

// 等价于
let man: {name: string; age: number} = {name: 'joye', age: 30};

总结:

接口代表接口类型,匿名接口代表对象类型

可选属性 & 只读属性

interface Person {
  name: string;
  // 注意此处的问号,age此时为可选属性
  age?: number;
  // 只读属性
  readonly key: string
}

接口的应用

接口最重要的作用在于描述一个复杂值的外形,通常情况下,接口可以描述:

  • 对象字面量
  • 函数
  • 可索引值
对象字面量
interface Person {
  name: string;
  age: number;
}

// 定义一个对象字面量male
let male = {
  name: 'joye',
  age: 30,
  gender: 'male'
};

// 正确,male包含Person接口的所有属性
let man: Person = male;

// 不正确
// error TS2322: Type '{ name: string; age: number; gender: string; }' is not assignable to type 'Person'. 
// Object literal may only specify known properties, and 'gender' does not exist in type 'Person'.
let man: Person = {
  name: 'joye',
  age: 30,
  gender: 'male'
};

对象字面量在直接赋值的时候,编译器会检查字面量类型是否完全匹配

描述函数
// 描述函数
interface MyFunc {
  (name: string, age: number): string; 
}

// 声明接口类型
let fn: MyFunc; 
// 等价于
let fn: { (name: string, age: number): string; } // 匿名接口
// 等价于
let fn: (name: string, age: number) => string; 

怎么判断两个类型是否兼容或者说等价?

让他们相互赋值

let fn1: MyFunc;
let fn2: (name: string, age: number) => string;

fn1 = fn2;  // OK
fn2 = fn1;  // OK

这里还有些坑?当在interface 含有protectedprivate成员,我们在进阶篇讲

描述可索引值
// 描述一个数组
interface StringArray {
  [index: number]: string;
}

// 声明接口类型
let myArray: StringArray;
// 等价于
let myArray: { [index: number]: string; }; // 匿名接口
// 等价于
let myArray: string[];

// 赋值
myArray = ["Bob", "Fred"];
描述对象
// 描述一个对象
interface MyObject {
  [index: string]: string;
}

// 声明接口类型
let myObject: MyObject;

// 赋值
myObject = {
  a: '1',
  b: '2',
  c: '3'
}
描述类数组对象
// 类数组对象
let obj = {
  1: 1,
  2: 2,
  name: 'ddd',
  age: 30
}

obj[1] === 1;
obj[2] === 2;
obj['name'] === 'ddd';
obj['age'] === 30;
描述类
// 定义一个类
class NewClass {}

// 用接口来描述这个类类型
interface MyClass {
  new(): NewClass;
}

// 声明一个变量为描述这个类的接口类型并初始化
let myClass: MyClass = NewClass;
// 等价于
let myClass: typeof NewClass = NewClass;

我们介绍到用接口来描述函数、可索引值、类类型,你会发现还不如直接用类型来声明更直接

// 声明函数
let myFunc: ()=>{};

// 声明数组
let myArr: string[];

// 声明类
class MyClass {}
let myClass: typeof MyClass;
用类来实现接口
interface Person {
  height: string;
  weight: string;
  run(): void;
}

class Man implements Person {
  height = '180cm';
  weight = '70kg';
  run() {
    console.log('run')
  }
}

实现类必须包含接口所声明的全部必选属性

接口继承

接口既可以继承接口,也可以继承类。

interface Shape {
  color: string;
}
interface Square extends Shape {
  sideLength: number;
}

// 正确,color 属性来自父接口
let square: Square = {
  color: 'blue',
  sideLength: 4
};

接口继承类

interface Shape {
  color: string;
}
interface Square extends Shape {
  sideLength: number;
}

class Size {
  size: string = '20cm'
}

interface CustomSqure extends Shape, Size {
  sideLength: number;
}

// 正确,color 属性来自父接口
let square: Square = {
  color: 'blue',
  sideLength: 4
};

// 正确, 若不加size  就会报错
let mySquere: CustomSqure = {
  color: 'blue',
  sideLength: 4,
  size: '10cm'
}

进阶篇(准备中)

JS向TS迁移

TS模块别名应用

TS类型映射

TS泛型

TS声明文件的书写

TS与小程序的深度结合

TS在小程序最佳实践(可能是)