保存成功
订阅成功
保存失败,请重试
提交成功

从零开始:全面了解 TypeScript 基础知识

一位善于写代码的厨师和车手。
查看本场Chat

前言

1 基础类型和变量声明

TypeScript 支持与 JavaScript 几乎相同的数据类型,只不过它会比 JavaScript 更为丰富,详情参考: http://www.typescriptlang.org/docs/handbook/basic-types.html

但在 TypeScript 的世界中因为推导的关系,我们可以不必明确给予类型,因为编译器会通过值来推导类型,大部分情况下这都是有用的,不过既然这是一个静态类型系统,我们更期望在书写的过程中能有非常明确的类型。

1.1 基础类型

let is: boolean = true;
let isN: number = 110;
let isS: string = "";
let isA: Array<any> = [];
let isNull: null = null;
let isUndefined: undefined = undefined;

上述的类型基本上 JavaScript 中都能找到对应的含义:

  • 布尔值
  • 数值
  • 字符串
  • 数组
  • null
  • undefined

其中数组还有一种简写,如:let isA: any[] = [],除此之外,TypeScript 在基本类型上扩充了一些其他非常有用的类型:

let x: [string, number] = ["hello", 110];
enum Color {
  Red,
  Green,
  Blue
}
let Ans: any = 4;
let Ans: any = "4";
let unusable: void = undefined;
const create = (o: object | null) => {

}
create({ prop: 0 });
function error(msg: string) never {
  throw new Error(msg);
}
  • 元组
  • 枚举
  • Any
  • Void
  • object
  • Never

多数情况下,元组 的使用倒是可以在函数返回多个值时有用,一般情况下,我们可能会使用不到它。

Never 来表示从未发生的值的类型,当你的函数在执行过程抛出了错误,并未到达返回时,即可使用。

Any 的情况是在未知类型的时候使用,不过程序一般都有比较明确脉络走向,我们可以使用泛型来代替 Any 去处理这样的状况,至于 Void 一般也是用于定义函数没有返回值时所用。

object 则是一种非基本类型的类型(这里的基本类型是值 JavaScript 中定义的基本类型),即任何不是number,string,boolean,symbol,null 或者 undefined 的类型。

对于类型断言来说有两种方式可以处理:

let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
let strLength2: number = (someValue as string).length;

<>as 关键字,这种情况是除非你非常明确这里的类型,在编译时编译器会进行类型断言。

在 TypeScript 扩充的这些基本类型中,比较常用的还是枚举,它不仅可以使用数字枚举,也可以字符串枚举,和其他语言中的枚举一样,它的值都有着明确特殊含义的,与你的编程逻辑密不可分。

enum Color {
  Red = "red",
  Green = "green"
}

const typed = document.getElementById("typed");
if (typed) {
  typed.style.background = Color.Red;
}

1.2 变量声明

在上述的文字中,其实你应该可以发现存在 let 关键字,接下来它与我想说的变量声明有非常大的关系。

在以往的 JavaScript 中 var 是我们来定义一个变量的开始,es2015 普及之后,letconst 几乎就成了我们编写代码时仅选择的两个变量声明方式,虽然某些特殊的场景下,我们也会使用到 var。说到变量的声明,其实要说到作用域的概念,这有一些比较枯燥的理论知识,所以简单的来说,不管是 let 还是 const 它的作用域都是块作用域,一个是可以在块作用域中随意赋值的 let,一个是一旦赋值就不可改变的 const。何谓,简单的理解可以为只要是 {} 包裹的区域,它就是一个块,在这个块中变量的声明是有一定规则的,最主要的规则就是:在块中声明的变量是无法被块作用域外的变量所访问,更多的理论知识可参考 http://www.typescriptlang.org/docs/handbook/variable-declarations.html

说到这里,其实我想说一说我们在写 es2015 时非常有用的一个特性:解构,在 TypeScript 中解构也是非常有用的一个特性:

let input = [1,2]
let [f, s] = input; // 数组解构
function a([f,s]:[number,number]){} // 用于函数的数组解构

a([1,2])
let input = [1,2,3,4];
let [f, ...s] = input;
//s [2,3,4] ...展开语法
let o = { a: 1, b: 2}

let { a, b} = o; // 对象解构

正常情况下,我们非常希望在对象的解构过程中给予一些类型,简单来说我们应该通过一个 interface 来定义一个对象,上述的情况也可以将类型赋值上去,只是代码看起来不是很满意,如:

let { a, b }: {a:number, b: number } = o;

2 枚举

枚举允许我们定义一组常量来表达和记录更明显的意图。

我们知道通常情况下网络会有三个状态,我们使用枚举来定义,如:

enum Network {
  OK,
  ERROR,
  TIMEOUT
}

在这里编译器会默认从 0 开始自动递增,这种情况对于我们不是很关心枚举的值时非常有用,但网络状态其实可以赋予一组更有意义的值,如:

enum Network {
  OK = 200,
  ERROR = 400,
  TIMEOUT = 408
}

很明显,我们可以用另外一个例子来举一反三,当我们需要对一个状态来表达 NoYes 时:

enum Status {
  Yes,
  No
}

我们也可以很简单的使用枚举,如:

const Http = (): Network => {
  return Network.OK;
}

有时候当数值无法表达我们的意图时,也可以选择字符串枚举来处理这个问题,只是字符串枚举将没有自增的行为,不过对于序列化来说明显会更有意义。

enum Color {
  red = "red",
  blue = "blue"
}

const color = `color:${Color.red};`;

TypeScript 给予枚举的能力并不限于此,有趣的是计算枚举并不是很常见,当然如果你愿意,你完全可以将它变成计算枚举,如:

enum FileAccess {
  // constant members
  None,
  Read    = 1 << 1,
  Write   = 1 << 2,
  ReadWrite  = Read | Write,
  // computed member
  G = "123".length
}

另外枚举成员还可以是类型,这些特殊的语义可以很好的表达:某些成员只能拥有枚举成员的值,如:

enum ShapeKind {
  Circle,
  Square,
}

interface Circle {
  kind: ShapeKind.Circle;
  radius: number;
}

interface Square {
  kind: ShapeKind.Square;
  sideLength: number;
}

let c: Circle = {
  kind: ShapeKind.Square,
  radius: 100,
}

你可以非常明确看到一行错误:

Type 'ShapeKind.Square' is not assignable to type 'ShapeKind.Circle'.

枚举的意义其实在于我们组织代码时对一些标识能有很明确的表达出意图,这种感觉比在 JavaScript 中要舒服很多。

3 函数

函数作为 JavaScript 世界中的一等公民,在描述如何执行操作中起到了关键作用。 TypeScript 在此之上为函数添加一些新的功能,让其更好用便成为了其意义的开始。

function addNums(x: number, y: number): number {
  return x + y;
}

const addNums1 = (x: number, y: number) => x + y;
const addNums2 = function(x: number, y: number){
  return x + y;
}

对于函数而言我们可以为参数,返回值添加其应有的类型,这对于函数的执行来说有很明确的意义,只是如果我们要输入完整的类型,它的长度有时会变的可怕,我更希望这是能可控的:

type AddFunction = (x: number, y: number) => number;

const d = (add: AddFunction): number => {
  return add(1,2);
}

接下来我们要去看一看 TypeScript 为其添加的些许新功能,除了类型之外。

3.1 可选和默认参数

在 TypeScript 中函数需要为每个参数定义类型,但这并不意味着不能被赋予未定义,有时候可选还是非常有用的:

const optional = (x?: number): number | undefined => {
  if (x){
    return x;
  }
}

optional()
optional(1)

当然,如果你很明确传入参数的意图也可以使用 ! 跳过编译器对它的检查。

在 TypeScript 中我们还可以为参数设置一个默认值,因为如果使用者没有传递参数时,未定义并不是我们想要的意义:

const defaultParams = (x = 1): number => {
  return x;
}

3.2 rest 参数

如果有时候你想将多个参数作为一个 group 来使用,这个时候 rest 参数才会有明显的意义:

const restParams = (x: number, ...options: number[]) => {

}
restParams(1);
restParams(1,2,3,4,5);

3.3 重载

JavaScript 中并没有关于重载的特性,但如果对于不同的参数返回不同的结果,这确却是有意义的:

function overloads(x: number): number;
function overloads(x: string): string;
function overloads(x: any): any {
  if (typeof x === "number") {
    return 0;
  }
  if (typeof x === "string") {
    return "0"
  }
}

overloads(1);
overloads("1");

3.4 this

由于 TypeScript 是 JavaScript 的超集,因此 this 的行为和 JavaScript 一样,但 TypeScript 为你提供了几种方式来捕获不正确的用法:

let deck = {
  suits: ["hearts", "spades", "clubs", "diamonds"],
  cards: Array(52),
  createCardPicker: function() {
    return function() {
      let pickedCard = Math.floor(Math.random() * 52);
      let pickedSuit = Math.floor(pickedCard / 13);
      return {suit: this.suits[pickedSuit], card: pickedCard % 13};
    }
  }
}

let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();
alert("card: " + pickedCard.card + " of " + pickedCard.suit);

由于我们手动调用了 createCardPicker ,这里的 this 在严格模式中将是 undefined 而不是 window,当你将配置项中的 noImplicitThis 设置为 true 时,编译器在编译时会为你给出一份警告:

'this' implicitly has type 'any' because it does not have a type annotation.
An outer value of 'this' is shadowed by this container.

通常对于此类情况的 fix 非常有参考价值,修复它也非常容易:

let deck = {
  suits: ["hearts", "spades", "clubs", "diamonds"],
  cards: Array(52),
  createCardPicker: function() {
    return () => {
      let pickedCard = Math.floor(Math.random() * 52);
      let pickedSuit = Math.floor(pickedCard / 13);
      return {suit: this.suits[pickedSuit], card: pickedCard % 13};
    }
  }
}

let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();
alert("card: " + pickedCard.card + " of " + pickedCard.suit);

当你使用 call 来改变 this 时,我们也应该期望给它一个明确的类型,有时它真的非常有用,比如:

class Call{
  say(){

  }
}
const call = new Call();
const func = function(this: Call){
  this.say();
}
func.call(call);

如果你时常使用 addClickListener 来监听事件,那么应该知道使用第一个参数 this:void 来描述 functhis,这非常有意义。

4 接口

类型检查属于 TypeScript 的核心原则之一,在 TypeScript 的世界里接口充当了命名这些类型的角色。

先让我们来看一个例子:

const printLabel = (o: { label: string, size: number }) => {
  //
}
printLabel({size: 10, label: ""});

多数情况下传递的对象参数可能会有多个属性,这也意味着当明确属性变为不可能时,它的长度将会特别可怕,要知道 type 和 接口的唯一区别就是接口可以被继承,接下来我们可以稍微改动一下:

interface ILable {
  label: string;
  size: number;
}

const label = (o: ILable) => {

}

label({ label: "", size: 10});

4.1 可选属性

有时候并非所有的属性都是必须的,因此我们需要让属性变成可选:

interface ILable {
  label: string;
  size?: number;
}

const label = (o: ILable) => {

}

label({ label: "", size: 10});
label({ label: ""})

4.2 只读属性

如果有些属性只能在对象首次创建时对其赋值,我们可以将其变成只读:

interface IPoint {
  readonly x: number;
  readonly y: number;
}

let point: IPoint = {
  x: 1,
  y: 10
};

如果再赋值会得到一行错误:

point.x = 1;
Cannot assign to 'x' because it is a read-only property.ts

4.3 函数类型

当我们需要给予某些函数类型时,我们就可以如下:

interface IFunction {
  (): void;
}

let funcs: IFunction = () => {}

4.4 可索引类型

正如我们用接口来描述函数类型一样,我们也可以使用接口来描述可索引的类型:

interface IArray {
  [index: number]: string;
}

let arr: IArray = ["1", "2"];
arr[0];

支持索引签名的类型有两种:

  • 字符串
  • 数字

4.5 Class 类型

我们可以在接口中描述一个方法或属性,然后在类里实现它:

interface IClass {
  date: Date;
  getTime: () => number;
}

class Time implements IClass {
  date = new Date();
  getTime(){
    return this.date.getTime();
  }
  constructor(){

  }
}
const time = new Time();

4.6 继承接口

和类一样,接口也是可以互相继承的,并且一个接口可以继承多个接口:

interface IA {
  a: string;
}

interface IB extends IA {
  b: number;
}

let exten: IB = {
  a: "1",
  b: 1
}

4.7 混合类型

先前我们提过接口是 TypeScript 来描述类型的核心原则之一,因此接口可以将多种类型混合起来:

interface IHybrid {
  name: string;
  age: number;
  add: () => void;
}

const hybrid: IHybrid = {
  name: "icepy",
  age: 0,
  add: function(){

  }
}

4.8 接口继承类

当接口继承一个类时它将会继承类的所有成员但不包括实现:

class Base {
  public type: string;
  constructor(type: string){
    this.type = type;
  }

  logType(){
    return this.type;
  }
}

interface IButton extends Base {}

class Button implements IButton {
  type = "button"
  constructor(){}
  logType(){
    return this.type;
  }
}

5 类

在没有出现 es2015 之前,在 JavaScript 中,我们都使用函数和原型来完成一个类的定义,但这对于熟悉其他面向对象的程序员(Java)来说非常的艰难,于是 es2015 将我们复杂的面向对象编程简化了不少,反而 TypeScript 对于它还有一些增强的扩展,我们可以一直使用它而不必等待下一版本的 JavaScript 标准实现,并且这和标准非常的类似。

让我们来看一个简单的基于类的例子:

class World {
  public country: string;
  private max: number;
  constructor(country: string){
    this.country = country;
    this.max = 110;
  }

  output() {
    return this.country;
  }
}

class Country extends World {
  constructor(country: string){
    super(country);
  }
}

在这个范例中,我们既定义了类也完成了继承,看起来是不是和 es2015 几乎一样呢?当然它也有一些与 es2015 的标准稍有不同,因为 TypeScript 增强了关于 public private 之类的一些定义。

5.1 修饰符

  • public 外部程序可以自由的访问
  • private 外部程序不可以自由的访问
  • protected 与 private 类似,唯一的不同是它可以在派生类中自由的访问
  • readonly 与词同意,只读
  • static 静态属性或方法
class StaticClass {
  static orig = "";
  static origFun = () => {}
}

5.2 存取器

如果你使用过 Object.defineProperty 那么一定理解如 getter setter 钩子的作用,在 TypeScript 中类也定义了非常类似的东西,举个例子:

class Employee {
  private _fullName: string;

  constructor(){
    this._fullName = "";
  }

  get fullName(): string {
    return this._fullName;
  }

  set fullName(newValue: string) {
    this._fullName = newValue;
  }
}

const emp = new Employee();
emp.fullName = "";

比对一下编译之后的代码:

var Employee = /** @class */ (function () {
  function Employee() {
    this._fullName = "";
  }
  Object.defineProperty(Employee.prototype, "fullName", {
    get: function () {
        return this._fullName;
    },
    set: function (newValue) {
        this._fullName = newValue;
    },
    enumerable: true,
    configurable: true
  });
  return Employee;
}());
var emp = new Employee();
emp.fullName = "";

是不是发现了 Object.defineProperty ? 在使用存取器的过程中,唯一要注意的是如果只定义了 get 钩子而没有定义 set 钩子的话,这个属性将是 readonly

5.3 抽象类

这个概念几乎在 JavaScript 从未存在过,如果有其他面向对象编程经验的程序应该会比较明白,说一个非常简单类似的设计,如iOS 中的 protocol:

abstract class Department {
  constructor(public name: string) {
  }

  printName(): void {
    console.log('Department name: ' + this.name);
  }

  abstract printMeeting(): void; // 必须在派生类中实现
}

class AccountingDepartment extends Department {

  constructor() {
    super('Accounting and Auditing'); // 在派生类的构造函数中必须调用 super()
  }

  printMeeting(): void {
    console.log('The Accounting Department meets each Monday at 10am.');
  }

  generateReports(): void {
    console.log('Generating accounting reports...');
  }
}

6 泛型

在软件工程领域,我们不仅要创建定义一致良好的API,也需要同时考虑重用性,泛型就给予了这样的灵活性又不失优雅。

让我们先来创建一个简单的泛型函数:

function r<T>(args: T): T {
  return args;
}
r("icepy");
r(100)
r(true)

当我们不知道返回类型时,泛型函数就解决了这样的问题,虽然这看起来和 Any 非常的类似。

由于我们定义的是泛型函数,因此它并不会丢失类型,反而会根据我们传入的参数类型而返回一个类型,这也意味着我们可以将它适用于多个类型。

6.1 为我们的 r 函数创建泛型类型

type GenericsR = <T>(args: T) => T;

function r<T>(args: T): T {
  return args;
}

const _r: GenericsR = r;

创建类型,其实仅是从编程风格上来说更统一和方便使用。

6.2 泛型类

泛型类其实看上去和定义一个泛型函数很类似,如:

class GenericsClass<T>{
  public add?: (x: T, y: T) => T;
}

const cls = new GenericsClass<number>();
cls.add = (x: number, y: number): number => {
  return x + y;
}

7 高级类型

从英译的文字来看 高级类型 并未有我们想象的那么复杂,这只是对于我们日常的编程生活中的一些补充,某些场景下,这些类型会为你的编程范式带来便捷。

当我们从最初的 mixins 中获取收益时,你就需要用到如下的一个类型了:

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

class Person {
  constructor(public name: string) { }
}
interface Loggable {
  log(): void;
}
class ConsoleLogger implements Loggable {
  log() {
    // ...
  }
}
var jim = extend(new Person("Jim"), new ConsoleLogger());
var n = jim.name;
jim.log();

如果你使用过 vue 的话,应该体验过 mixins 带来的便捷性。

当我们写了一个函数,它的参数预期可能是 number 也可能是 string ,除了泛型的方式之外,我们也可以使用这样的方式来处理这个问题:

function sumStr(x: number | string): string {
  if (typeof x === "number") {
    return `${x}`;
  }
  if (typeof x === "string") {
    return `${x}`;
  }
  throw new Error("not number or string");
}

从符号上来说 x 可能是 number 也可能是 string ,如果是其他类型,就抛出一个错误。

我们再来看一个稍微复杂一些的范例:

class A {
  log(){
    console.log("A");
  }
}

class B {
  logg(){
    console.log("B");
  }
}

function getx(): A | B {
  return new A();
}

const x = getx();

(<A>x).log();

如果当你返回两个不同的类型时,为了让这段代码可以工作,我们需要断言一个明确的类型,然后才开始工作。

与 JavaScript 一样,我们都使用 typeofinstanceof 来做类型保护,用于判断当一个变量是未知时的类型判断。这些预期的行为与 JavaScript 没有任何区别,因此我们应该去更好的理解 instanceof 来处理对象。

大部分情况下,如果没有使用接口来定义类型,我们为会比较杂乱的类型定义一个别名,这对于程序的可阅读性有较高的提升,如:

type info = {
  name: string;
  age: number;
  x: string;
}

let a: info = {
  name: "",
  age: 0,
  x: ""
}

let b: {
  name: string;
  age: number;
  x: string;
} = {
  name: "",
  age: 0,
  x: ""
}

你能所预期的,如果没有别名,这些不是接口定义的类型,随着属性的增加会恐怖到什么地步。既然我们提到了接口,那么有一些不一样的地方,那是它与别名的区别,别名不能像接口那样被继承和被实现。

8 Iterators and Generators

如果一个对象具备 Symbol.iterator 的实现,则认为该对象是可迭代的,一些内置的类型如:ArrayMapSet 等。

8.1 for..of

for..of 会遍历可迭代对象,调用对象上的 Symbol.iterator 方法,下面是在数组上使用 for..of 的简单例子:

let someArray = [1, "string", false];

for (let entry of someArray) {
  console.log(entry); // 1, "string", false
}

8.2 Generators

如果生成的代码目标是 ES5ES3,迭代器只允许在 Array 上使用,如果是其他对象,除非已经实现了 Symbol.iterator 方法。

如果是 ECMAScript 2015 时,编译器会生成对应引擎的内置实现。

9 模块

从 es2015 开始 JavaScript 有了自己的模块,TypeScript 也遵循了这样的定义,其实在 TypeScript 还有一个命名空间的概念,这样的概念应该说从历史遵循而来,上了年纪的前端程序员应该会对YUI有一些印象,当年的前端对于命名空间的运用,这是最出色的一款框架。不过,今天我们将讲一讲 TypeScript 中的模块,以及另外一些模块规范的第三方包,该如何在 TypeScript 中引用。

对于模块而言它自身是运行在一个局部的作用域中的,这一点非常重要,这也意味着在一个模块中的变量,函数,类等,除非你导出,外部程序是无法使用的,因此 TypeScript 也遵循了 importexport 的机制,反之如果一个文件内没有这两个关键字存在的话,那么它的内容将会被视为全局。

导出一个常量:

export const D = "";

导入一个常量:

import { D } from "xx";

导出关键字和导入关键字都适用于重命名,如:

import { D as Dell } from "xx"

const C = "";
export { C as D }

我们也可以将所有的导出都导入到一个变量中,如:

export const A = "";
export const B = "";
import * as CONST from "xx";

每一个模块都可以使用一个默认的导出,如:

export default function reducers () {}
import reducers from "xx";

10 其他

如果你使用过 React 那么就应该对 jsx 非常的熟悉,这是一种嵌入式类似XML语法的可转换成 JavaScript 的设计实现,它非常的灵活在 JavaScript 中,我个人非常的喜欢 jsx,完全符合我自己的口味,菜好不好吃,只有自己去试试才知道。

TypeScript 也对于 jsx 有三种模式的支持,我们可以在 tscnfig.json 文件中找到:

// "jsx": "preserve",  /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
  • preserve
  • react-native
  • react

要启用 jsx 在 TypeScript 的世界中需要做两件事情:

  • 使用 .tsx 后缀的文件
  • tsconfig.json 配置文件中启用 jsx 选项

这三种模式下,各有不同。

  • preserve 模式下对代码的编译会保留 jsx 格式,并输出一份 .jsx 后缀的文件。
  • react 模式下对代码的编译是直接转换成 React.createElement ,并输出一份 .js 后缀的文件。
  • react-native 模式下对代码的编译会保留 jsx 格式,并输出一份 .js 后缀的文件。

其他的使用方式几乎一样,并未有过多需要注意的地方,目前 TypeScript 3.0 已经支持了 defaultProps ,这也意味着我们可以很方便的定义默认值,不必像以前那样搞的很复杂,这一点上来说是新版本的 TypeScript 对 react 更好的支持。

类型检查还是会和正常的 TypeScript 一样(.ts 后缀的文件名),因此我们不必过于担心。


本文首发于 GitChat,未经授权不得转载,转载需与 GitChat 联系。

还没有评论
评论
查看更多
微信扫描登录