一、声明文件

当使用第三方库时,我们需要引用它的声明文件。

声明语句

假如我们想使用第三方库,比如 jQuery,我们通常这样获取一个 id 是 foo 的元素:

jQuery('#foo');

// index.ts(1,1): error TS2304: Cannot find name 'jQuery'.

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

jQuery('#foo');

declare 定义的类型只会用于编译时的检查,编译结果中会被删除。

声明文件

通常我们会把类型声明放到一个单独的文件中,这就是声明文件:

// jQuery.d.ts

declare var jQuery: (string) => any;

我们约定声明文件以 .d.ts 为后缀。

然后在使用到的文件的开头,用「三斜线指令」表示引用了声明文件:

/// <reference path="./jQuery.d.ts" />

jQuery('#foo');

第三方声明文件

npm install @types/jquery --save-dev

二、命名空间

“内部模块”现在称做“命名空间”。 “外部模块”现在则简称为“模块”,这是为了与 ECMAScript 2015里的术语保持一致

namespace Utility {
  export function log(msg) {
    console.log(msg);
  }
  export function error(msg) {
    console.log(msg);
  }
}

// usage
Utility.log('Call me');
Utility.error('maybe');

命名空间:代码层面的归类和管理。将有相似功能的代码都归一到同一个空间下进行管理,方便其他代码引用。更多的是侧重代码的复用。

模块:一个完整功能的封装,对外提供的是一个具有完整功能的功能包,需要显式引用。一个模块里可能会有多个命名空间。

三、interface 与 type

相同点

都可以描述一个对象或者函数。

interface
interface User {
  name: string
  age: number
}

interface SetUser {
  (name: string, age: number): void;
}
type
type User = {
  name: string
  age: number
};

type SetUser = (name: string, age: number): void;

都允许拓展(extends)

interface 和 type 都可以拓展,并且两者并不是相互独立的,也就是说 interface 可以 extends type, type 也可以 extends interface 。 ==虽然效果差不多,但是两者语法不同。==

interface extends interface
interface Name { 
  name: string; 
}
interface User extends Name { 
  age: number; 
}
type extends type
type Name = { 
  name: string; 
}
type User = Name & { age: number  };
interface extends type
type Name = { 
  name: string; 
}
interface User extends Name { 
  age: number; 
}
type extends interface
interface Name { 
  name: string; 
}
type User = Name & { 
  age: number; 
}

不同点

type 可以而 interface 不行

type 可以声明基本类型别名,联合类型,元组等类型

// 基本类型别名
type Name = string;
// 联合类型
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
    if (typeof n === 'string') {
        return n;
    } else {
        return n();
    }
}

type 语句中还可以使用 typeof 获取实例的 类型进行赋值

// 当你想获取一个变量的类型时,使用 typeof
let div = document.createElement('div');
type B = typeof div
interface 可以而 type 不行

interface 能够声明合并

interface User {
  name: string
  age: number
}

interface User {
  sex: string
}

/*
User 接口为 {
  name: string
  age: number
  sex: string 
}
*/

一般来说,如果不清楚什么时候用interface/type,能用 interface 实现,就用 interface , 如果不能就用 type

四、元组

数组合并了相同类型的对象,而元组(Tuple)合并了不同类型的对象。

let xcatliu: [string, number];
xcatliu[0] = 'Xcat Liu';
xcatliu[1] = 25;

let xcatliu: [string, number];
xcatliu = ['Xcat Liu', 25];

let xcatliu: [string, number] = ['Xcat Liu'];

// index.ts(1,5): error TS2322: Type '[string]' is not assignable to type '[string, number]'.
//   Property '1' is missing in type '[string]'.

五、枚举

枚举(Enum)类型用于取值被限定在一定范围内的场景

常量统一使用枚举定义,统一使用全大写加_定义

数字类型枚举

枚举成员会被赋值为从 0 开始递增的数字,同时也会对枚举值到枚举名进行反向映射:

enum Direction {
    Up,
    Down,
    Left,
    Right,
}
console.log(Drection['Up']);    // 0

enum Direction {
    Up = 1,
    Down,
    Left,
    Right,
}
console.log(Drection['Up']);    // 1

enum ECode {
  OPERATION_SUCCESS = 10000, // "操作成功"
  GAIN_SUCCESS = 10001, // "成功获取数据,数据非空"
  GAIN_SUCCESS_EMPTY = 10002, // "操作成功,数据为空"
}

字符串枚举

enum EvidenceTypeEnum {
  UNKNOWN = '',
  PASSPORT_VISA = 'passport_visa',
  PASSPORT = 'passport',
  SIGHTED_STUDENT_CARD = 'sighted_tertiary_edu_id',
  SIGHTED_KEYPASS_CARD = 'sighted_keypass_card',
  SIGHTED_PROOF_OF_AGE_CARD = 'sighted_proof_of_age_card'
}
console.log(EvidenceTypeEnum.PASSPORT); // passport

六、泛型

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

我们来实现一个函数 createArray,它可以创建一个指定长度的数组,同时将每一项都填充一个默认值:

function createArray<T>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

createArray<string>(3, 'x'); // ['x', 'x', 'x']

多个类型参数

function swap<T, U>(tuple: [T, U]): [U, T] {
    return [tuple[1], tuple[0]];
}

swap([7, 'seven']); // ['seven', 7]

我们定义了一个 swap 函数,用来交换输入的元组。

泛型接口

interface CreateArrayFunc<T> {
    (length: number, value: T): Array<T>;
}

let createArray: CreateArrayFunc<any>;
createArray = function<T>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

createArray(3, 'x'); // ['x', 'x', 'x']

泛型类

// 创建一个泛型类
class Queue<T> {
    private data = [];
    push = (item: T) => this.data.push(item);
    pop = (): T => this.data.shift();
}

泛型参数的默认类型

在 TypeScript 2.3 以后,我们可以为泛型中的类型参数指定默认类型。当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用。

function createArray<T = string>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

你想用它来提供什么样的约束。如果你不能很好的回答它,你可能会误用泛型, 如

function foo<T>(arg: T): void;

使用如下方式可能更好

function foo(arg: any): void;

七、readonly

interface IFoo {
  readonly name : 1
  bar: number
  bas: number
}

// 数组 
let foo: ReadonlyArray<number> = [1, 2, 3]
console.log(foo[0]) // ok
foo.push(4); // Error: ReadonlyArray 上不存在push,因为他会改变数组
foo = foo.concat(4) // ok, 创建了一个复制

// 项目统一使用
import React, { PureComponent } from 'react'
const initialState = {}
interface IProps {}
interface IState {}
export default class Demo extends PureComponent<IProps, IState> {
   readonly state: IState = initialState
   render() {
       return (
           <div></div>
       )
   }
}

const 的不同

const

readonly

const foo = 123; // 变量

let bar: {
  readonly len: number; // 属性
};

八、联合类型

联合类型(Union Types)表示取值可以为多种类型中的一种。

interface ISquare {
  kind: 'square'
  size: number
}

interface IRectangle {
  kind: 'rectangle'
  width: number
  height: number
}

type TShape = ISquare | IRectangle

// 条件判断
function area(s: TShape) {
  if (s.kind === 'square') {
    // 现在 TypeScript 知道 s 的类型是 Square
    // 所以你现在能安全使用它
    return s.size * s.size
  }
  // 不是一个 square ?因此 TypeScript 将会推算出 s 一定是 Rectangle
  return s.width * s.height
}
    
//  switch
function area(s: TShape) {
  switch (s.kind) {
    case 'square':
      return s.size * s.size;
    case 'rectangle':
      return s.width * s.height;
    default:
      const _exhaustiveCheck: never = s;
  }
}

area({kind:'square', size: 1})
area({kind:'rectangle', size: 1}) // Error
area({kind:'rectangle', width: 1, height: 2})
area({kind:'square', width: 1, height: 2}) // Error

九、类是有用的

function foo() {
  let someProperty;

  // 一些其他的初始化代码

  function someMethod() {
    // 用 someProperty 做一些事情
    // 可能有其他属性
  }

  // 可能有其他的方法
  return {
    someMethod
    // 可能有其他方法
  };
};

上面是在应用中常见的闭包,可以使用类的方式来重写

class Foo {
  public someProperty;

  constructor() {
    // 一些初始化内容
  }

  public someMethod() {
    // ..code
  }

  public someUtility() {
    // .. code
  }
}

export = new Foo();

这并不仅仅有利于开发者,在创建基于类的更出色可视化工具中,它更常见。并且,这有利于项目的理解和维护。

十、明确的类型转换

if (123) {
  // 将会被推断出 `true`
  console.log('Any number other than 0 is truthy');
}

通过操作符 !!,你可以很容易的将某些值转化为布尔类型的值。

// Direct variables
const hasName = !!name;

// As members of objects
const someObj = {
  hasName: !!name
};

// ReactJS
{
  !!someName && <div>{someName}</div>;
}

十一、函数参数

如果一个函数含有很多参数或者相同类型参数,可以使用对象的形式来替代这些函数参数:

function foo(flagA: boolean, flagB: boolean) {
  // 函数主体
}

像这样的函数,你可能会很容易错误的调用它,如 foo(flagB, flagA),并且你并不会从编译器得到想要的帮助。

使用接收一个对象参数的形式:

function foo(
  config: {
    flagA: boolean;
    flagB: boolean;
  }
) {
  const { flagA, flagB } = config;
}

现在,函数将会被 foo({ flagA, flagB }) 的形式调用,这样有利于发现错误及代码审查。

十二、对象字面量的惰性初始化

在 JavaScript 中,像这样用字面量初始化对象的写法十分常见:

let foo = {};
foo.bar = 123;
foo.bas = 'Hello World';

但在 TypeScript 中,同样的写法就会报错:

let foo = {};
foo.bar = 123; // Error: Property 'bar' does not exist on type '{}'
foo.bas = 'Hello World'; // Error: Property 'bas' does not exist on type '{}'

这是因为 TypeScript 在解析 let foo = {} 这段赋值语句时,会进行“类型推断”:它会认为等号左边 foo 的类型即为等号右边 {} 的类型。由于 {} 本没有任何属性,因此,像上面那样给 foo 添加属性时就会报错。

// 最好的解决方案

let foo = {
  bar: 123,
  bas: 'Hello World'
};

// 折中方案
interface Foo {
  bar: number;
  bas: string;
}

let foo = {} as Foo;
foo.bar = 123;
foo.bas = 'Hello World';