TypeScript学习(TypeScript类型系统)

TypeScript类型系统

基本注解

在下面这个例子中,使用了变量、函数参数以及函数返回值的类型注解:

const num: number = 123;
function identity(num: number): number {
  return num;
}

原始类型

let num: number;
let str: string;
let bool: boolean;

num = 123;
num = 123.456;
num = '123'; // Error

str = '123';
str = 123; // Error

bool = true;
bool = false;
bool = 'false'; // Error

数组

TypeScript 为数组提供了专用的类型语法,因此你可以很轻易的注解数组。它使用后缀 [], 接着你可以根据需要补充任何有效的类型注解(如::boolean[])。它能让你安全的使用任何有关数组的操作,而且它也能防止一些类似于赋值错误类型给成员的行为。如下所示:

let boolArray: boolean[];

boolArray = [true, false];
console.log(boolArray[0]); // true
console.log(boolArray.length); // 2

boolArray[1] = true;
boolArray = [false, false];

boolArray[0] = 'false'; // Error
boolArray = 'false'; // Error
boolArray = [true, 'false']; // Error

接口(定义一整个接口的类型)

接口是 TypeScript 的一个核心知识,它能合并众多类型声明至一个类型声明(个人理解为策略模式):

interface Name {
  first: string;
  second: string;
}

let name: Name;
name = {
  first: 'John',
  second: 'Doe'
};

name = {
  // Error: 'Second is missing'
  first: 'John'
};

name = {
  // Error: 'Second is the wrong type'
  first: 'John',
  second: 1337
};

内联类型注释(单次使用,无需使用接口抽象)

与创建一个接口不同,你可以使用内联注解语法注解任何内容::{ /Structure/ }:

let name: {
  first: string;
  second: string;
};

name = {
  first: 'John',
  second: 'Doe'
};

name = {
  // Error: 'Second is missing'
  first: 'John'
};

name = {
  // Error: 'Second is the wrong type'
  first: 'John',
  second: 1337
};

内联类型能为你快速的提供一个类型注解。它可以帮助你省去为类型起名的麻烦(你可能会使用一个很糟糕的名称)。然而,如果你发现需要多次使用相同的内联注解时,那么考虑把它重构为一个接口(或者是 type alias,它会在接下来的部分提到)是一个不错的主意。

特殊类型

除了被提到的一些原始类型,在 TypeScript 中,还存在一些特殊的类型,它们是 any、 null、 undefined 以及 void。

any

any 类型在 TypeScript 类型系统中占有特殊的地位。它提供给你一个类型系统的「后门」,TypeScript 将会把类型检查关闭。在类型系统里 any 能够兼容所有的类型(包括它自己)。因此,所有类型都能被赋值给它,它也能被赋值给其他任何类型。以下有一个证明例子:

let power: any;

// 赋值任意类型
power = '123';
power = 123;

// 它也兼容任何类型
let num: number;
power = num;
num = power;

当你把 JavaScript 迁移至 TypeScript 时,你将会经常性使用 any。但你必须减少对它的依赖,因为你需要确保类型安全。当使用 any 时,你基本上是在告诉 TypeScript 编译器不要进行任何的类型检查。

null 和 undefined

在类型系统中,Javascript中的null和undefined字面量和其他被标注了any类型的变量一样,都能被赋值给任意类型的变量。

// strictNullChecks: false

let num: number;
let str: string;

// 这些类型能被赋予
num = null;
str = undefined;

void

使用:void来表示一个函数没有一个返回值(常用)

function log(message: string): void {
  console.log(message);
}

泛型(不确定是哪种类型,重点)

在计算机科学中,许多算法和数据结构并不会依赖于对象的实际类型。但是,你仍然会想在每个变量里强制提供约束。例如:在一个函数中,它接受一个列表,并且返回这个列表的反向排序,这里的约束是指传入至函数的参数与函数的返回值:

function reverse<T>(items: T[]): T[] {
  const toreturn = [];
  for (let i = items.length - 1; i >= 0; i--) {
    toreturn.push(items[i]);
  }
  return toreturn;
}

const sample = [1, 2, 3];
let reversed = reverse(sample);

console.log(reversed); // 3, 2, 1

// Safety 
reversed[0] = '1'; // Error
reversed = ['1', '2']; // Error

reversed[0] = 1; // ok
reversed = [1, 2]; // ok

在没有执行reversed之前,并不确定数组内的类型是number类型,但是再传递一个number类型的数组之后,类型就被确定,再次进行修改时就必须按照规定的数据类型进行修改
在上个例子中,函数 reverse 接受一个类型为 T(注意在 reverse 中的类型参数) 的数组(items: T[]),返回值为类型 T 的一个数组(注意:T[]),函数 reverse 的返回值类型与它接受的参数的类型一样。当你传入 const sample = [1, 2, 3] 时,TypeScript 能推断出 reverse 为 number[] 类型,从而能给你类型安全。与此相似,当你传入一个类型为 string[] 类型的数组时,TypeScript 能推断 reverse 为 string[] 类型,如下例子所示:

const strArr = ['1', '2'];
let reversedStrs = reverse(strArr);

reversedStrs = [1, 2]; // Error

事实上,JavaScript 数组已经拥有了 reverse 的方法,TypeScript 也确实使用了泛型来定义其结构

interface Array<T> {
  reverse(): T[];
}

这意味着,当你在数组上调用 .reverse 方法时,将会获得类型安全:

let numArr = [1, 2];
let reversedNums = numArr.reverse();

reversedNums = ['1', '2']; // Error

联合类型

在 JavaScript 中,你可能希望属性为多种类型之一,如字符串或者数组。这正是 TypeScript 中联合类型能派上用场的地方(它使用 | 作为标记,如 string | number)。关于联合类型,一个常见的用例是一个可以接受字符串数组或单个字符串的函数:

function formatCommandline(command: string[] | string) {
  let line = '';
  if (typeof command === 'string') {
    line = command.trim();
  } else {
    line = command.join(' ').trim();
  }

  // Do stuff with line: string
}

交叉类型

在 JavaScript 中, extend 是一种非常常见的模式,在这种模式中,你可以从两个对象中创建一个新对象,新对象拥有着两个对象所有的功能。交叉类型可以让你安全的使用此种模式:

function extend<T extends object, U extends object>(first: T, second: U): T & U {
  const result = <T & U>{};
  for (let id in first) {
    (<T>result)[id] = first[id];
  }
  for (let id in second) {
    if (!result.hasOwnProperty(id)) {
      (<U>result)[id] = second[id];
    }
  }

  return result;
}

const x = extend({ a: 'hello' }, { b: 42 });

// 现在 x 拥有了 a 属性与 b 属性
const a = x.a;
const b = x.b;

元组类型

JavaScript 并不支持元组,开发者们通常只能使用数组来表示元组。而 TypeScript 支持它,开发者可以使用 :[typeofmember1, typeofmember2] 的形式,为元组添加类型注解,元组可以包含任意数量的成员,示例:

let nameNumber: [string, number];

// Ok
nameNumber = ['Jenny', 221345];

// Error
nameNumber = ['Jenny', '221345'];

// 将其与 TypeScript 中的解构一起使用:
let nameNumber: [string, number];
nameNumber = ['Jenny', 322134];

const [name, num] = nameNumber;

类型别名

TypeScript 提供了为类型注解设置别名的便捷语法,你可以使用 type SomeName = someValidTypeAnnotation 来创建别名:

type StrOrNum = string | number;

// 使用
let sample: StrOrNum;
sample = 123;
sample = '123';

// 会检查类型
sample = true; // Error

与接口不同,你可以为任意的类型注解提供类型别名(在联合类型和交叉类型中比较实用),下面是一些能让你熟悉类型别名语法的示例。

type Text = string | { text: string };
type Coordinates = [number, number];
type Callback = (data: string) => void;

TIP

如果你需要使用类型注解的层次结构,请使用接口。它能使用 implements 和 extends
为一个简单的对象类型(如上面例子中的 Coordinates)使用类型别名,只需要给它一个语义化的名字即可。另外,当你想给联合类型和交叉类型提供一个语义化的名称时,一个类型别名将会是一个好的选择。

如何从javascript 迁移代码

首先,假设如下: 你了解 JavaScript;你了解在项目中常用的方式和构建工具(如:webpack)。一般来说,将 JavaScript 代码迁移至 TypeScript 包括以下步骤:

  • 添加一个 tsconfig.json 文件;
  • 把文件扩展名从 .js 改成 .ts,开始使用 any 来减少错误;
  • 开始在 TypeScript 中写代码,尽可能的减少 any 的使用;
  • 回到旧代码,开始添加类型注解,并修复已识别的错误;
  • 为第三方 JavaScript 代码定义环境声明。

记住:所有的 JavaScript 代码都是有效的 TypeScript 代码。这意味着,如果让 TypeScript 编译器编译 TypeScript 里的 JavaScript 代码,编译后的结果将会与原始的 JavaScript 代码一模一样。也就是说,把文件扩展名从 .js 改成 .ts 将不会造成任何负面的影响。

减少错误

  • 代码被迁移至 TypeScript 后,TypeScript 将会立即对你的代码进行类型检查,你的 JavaScript 代码可能并不像想象中那样整齐了,因此你可能会收到一些报错信息。这时,可以使用 any 来解决大部分的报错问题:
    虽然你可以用any暂时解决所有的类型检验问题,但是既然你选择了ts,那么除非情况特殊,否则不允许使用any类型
let foo = 123;
let bar = 'hey';

bar = foo; // Error: 不能把 number 类型赋值给 string 类型
  • 虽然这些错误是有效的,并且在大多数情况下,根据这些错误所推断出的信息比代码库的不同部分的原始作者想象的更好,但是你的重点是在逐步更新旧代码库的同时,用 TypeScript 编写新代码。在这里,你可以使用类型断言来减少此错误:
let foo = 123;
let bar = 'hey';

bar = foo as any; // ok
  • 从另一方面来说,你可能想用 any 用作类型注解:
function foo() {
  return 1;
}

let bar = 'hey';
bar = foo(); // Error: 不能把一个 number 类型赋值给 string 类型
// 减少这种错误:
function foo(): any {
  // 添加 'any'
  return 1;
}

let bar = 'hey';
bar = foo();

第三方代码

你可以将你的 JavaScript 代码改成 TypeScript 代码,但是你不能让整个世界都使用 TypeScript。这正是 TypeScript 环境声明支持的地方。我建议你以创建一个 vendor.d.ts 文件作为开始(.d.ts 文件扩展名指定这个文件是一个声明文件),然后我向文件里添加东西。或者,你也可以创建一个针对于特定库的声明文件,如为 jquery 创建 jquery.d.ts 文件。

NOTICE

  • 几乎排名前 90% 的 JavaScript 库的声明文件存在于 DefinitelyTyped 仓库里,在创建自己定义的声明文件之前,我们建议你先去仓库中寻找是否有对应的声明文件。尽管如此,创建一个声明文件这种快速但不好的方式是减小使用 TypeScript 初始阻力的重要步骤
  • 以jQuery为例(在jquery.d.ts文件中):
declare var $: any;
// 有时,你可能想在某些内容(如 jQuery)上添加显式的注解,并且你会在类型声明空间中使用它。你可以通过 type 关键字快速的实现它:
declare type JQuery = any;
declare var $: JQuery;

第三方的NPM模块

  • 与全局变量声明相似,你可以快速的定义一个全局模块,如:对于 jquery,如果你想把它作为一个模块来使用(NPM),可以自己通过以下方式实现:
  declare module 'jquery';
  • 然后你就可以在必要时导入它:
import * as $ from 'jquery';

额外的非 JavaScript 资源

  • 在 TypeScript 中,甚至可以允许你导入任何文件,例如 .css 文件(如果你使用的是 webpack 样式加载器或 css 模块),你只要添加如下代码(放在 global.d.ts):
declare module '*.css';

现在你可以使用 import * as foo from ‘./some/file.css’。与此相似,如果你想使用 html 模版(例如:angular),你可以:

declare module '*.html';

使用@types(进行全局或者模块类型定义)

毫无疑问,DefinitelyTyped 是 TypeScript 最大的优势之一,社区已经记录了 90% 的顶级 JavaScript 库。这意味着,你可以非常高效地使用这些库,而无须在单独的窗口打开相应文档以确保输入的正确性。

  • 你可以通过 npm 来安装使用 @types,例如为 jquery 添加声明文件:
	npm install @types/jquery --save-dev
  • 全局 @types
    默认情况下,TypeScript 会自动包含支持全局使用的任何声明定义。例如,对于 jquery,你应该能够在项目中开始全局使用 $。
  • 模块 @types
    安装完之后,不需要特别的配置,你就可以像使用模块一样使用它:
import * as $ from 'jquery';

// 现在你可以此模块中任意使用$了 :)
  • 可以看出,对于某些团队而言,拥有允许全局使用的定义是一个问题。因此,你可以通过配置 tsconfig.json 的 compilerOptions.types 选项,引入有意义的类型:
{
  "compilerOptions": {
    "types" : [
      "jquery"
    ]
  }
}

如上例所示,通过配置 compilerOptions.types: [ “jquery” ] 后,只允许使用 jquery 的 @types 包,即使这个人安装了另一个声明文件,比如 npm install @types/node,它的全局变量(例如 process)也不会泄漏到你的代码中,直到你将它们添加到 tsconfig.json 类型选项。

总结

  • TypeScript 中的数据类型有Undefined、Number(数值类型)、string(字符串类型)、Boolean(布尔类型)、enum(枚举类型)、any(任意类型)、void(空类型)用于没有返回值的函数、Array(数组类型)、Tuple(元祖类型)、Null(空类型)。
  • 前期使用any来迁移我们的js代码
  • 使用@type进行全局或者模块类型定义,jQuery,lodash,i18n等等

你可能感兴趣的:(typescript,javascript,前端)