Skip to content

TypeScript

2023-04-06
Author:lzugis

什么是TypeScript

TypeScript是JavaScript的超集,意味着它能做JavaScript所做的一切,但有一些附加功能。

使用TypeScript的主要原因是为JavaScript添加静态类型。静态类型意味着变量的类型在程序中的任何时候都不能被改变。它可以防止大量的bug!

另一方面,JavaScript是一种动态类型的语言,意味着变量可以改变类型。这里有一个例子:

ts
// JavaScript
let foo = "hello";
foo = 55; // foo has changed type from a string to a number - no problem

// TypeScript
let foo = "hello";
foo = 55; // ERROR - foo cannot change from string to number

优缺点

为什么要使用TypeScript

  • 研究表明,TypeScript可以发现15%的常见错误。
  • 可读性--更容易看到代码应该做什么。而在团队中工作时,更容易看到其他开发人员的意图。
  • 它很受欢迎--了解TypeScript将使你能够申请到更多好工作。
  • 学习TypeScript会让你对JavaScript有更好的理解,并有新的视角。

TypeScript的缺点

  • TypeScript的编写时间比JavaScript长,因为你必须指定类型,所以对于较小的单独项目,可能不值得使用它。
  • TypeScript必须进行编译--这可能需要时间,特别是在大型项目中。

但是,你必须花更多的时间来写更精确的代码和编译,这将使你的代码中的错误减少得更多。 对于许多项目--尤其是中大型项目--TypeScript将为你节省大量的时间和麻烦。

如何设置TypeScript项目

安装Node和TypeScript编译器

首先,确保你的机器上全局安装了Node。

然后通过运行以下命令在你的机器上全局安装TypeScript编译器。

shell
# 全局安装ts
npm i -g typescript
# 检查安装版本
tsc -v

编译TypeScript

创建一个TypeScript文件(例如,index.ts), 编写一些JavaScript或TypeScript

ts
let sport = 'football';
let id = 5;

可以用以下命令将其编译成JavaScript:

shell
tsc index

TSC将把代码编译成JavaScript,并在一个名为index.js的文件中输出。如果你想指定输出文件的名称:

shell
tsc index.ts --outfile file-name.js

如果你想让TSC自动编译你的代码,每当你做了一个改动,请添加 "watch "(缩写 w)标志

shell
tsc index.ts -w

TypeScript的一个有趣之处在于,当你在编码时,它会在文本编辑器中报告错误,但它总是会编译你的代码,无论是否有错误。

ts
var sport = 'football';
var id = 5;

id = '5'; // Error: Type 'string' is not assignable to type 'number'.

但如果我们尝试用tsc index来编译这段代码,尽管有错误,但代码仍然可以编译。

如何设置ts配置文件

ts 配置文件应该在你项目的根目录下。在这个文件中,我们可以指定根文件,编译器选项,以及我们希望 TypeScript 在检查我们项目时有多严格。

首先,创建 ts 配置文件:

ts
tsc --init

你现在应该在项目根部有一个tsconfig.json文件。

这里有一些需要注意的选项(如果使用带有TypeScript的前端框架,这些东西大部分都是为你准备的)。

json
{
    "compilerOptions": {
        ...
        /* Modules */
        "target": "es2016", // Change to "ES2015" to compile to ES6
        "rootDir": "./src", // Where to compile from
        "outDir": "./public", // Where to compile to (usually the folder to be deployed to the web server)
        
        /* JavaScript Support */
        "allowJs": true, // Allow JavaScript files to be compiled
        "checkJs": true, // Type check JavaScript files and report errors
        
        /* Emit */
        "sourceMap": true, // Create source map files for emitted JavaScript files (good for debugging)
         "removeComments": true, // Don't emit comments
    },
    "include": ["src"] // Ensure only files in src are compiled
}

编译所有内容并观察变化:

shell
tsc -w

TS速查手册

TypeScript中的类型

原始类型

在JavaScript中,原始值是指不属于对象且没有方法的数据。有7种原始数据类型。

  • string
  • number
  • bigint
  • boolean
  • undefined
  • null
  • symbol

TypeScript,我们可以在声明一个变量后添加: type(称为 "类型注解 "或 "类型签名")来设置我们希望变量的类型。

ts
let id: number = 5;
let firstname: string = 'danny';
let hasDog: boolean = true;

let unit: number; // Declare variable without assigning a value
unit = 5;

但通常最好不要明确说明类型,因为TypeScript会自动推断变量的类型(类型推理):

ts
let id = 5; // TS knows it's a number
let firstname = 'danny'; // TS knows it's a string
let hasDog = true; // TS knows it's a boolean

hasDog = 'yes'; // ERROR

我们也可以将一个变量设定为能够成为联合类型联合类型是一个可以被分配到多个类型的变量:

ts
let age: string | number;
age = 26;
age = '26';

引用类型

在JavaScript中,几乎 "所有东西 "都是一个对象。事实上(而且令人困惑的是),如果用new关键字定义的话,字符串、数字和布尔都可以成为对象:

js
let firstname = new String('Danny');
console.log(firstname); // String {'Danny'}

但是,当我们谈论JavaScript中的引用类型时,我们指的是数组、对象和函数。

ts中的数组

在TypeScript中,你可以定义一个数组可以包含哪些类型的数据:

ts
let ids: number[] = [1, 2, 3, 4, 5]; // can only contain numbers
let names: string[] = ['Danny', 'Anna', 'Bazza']; // can only contain strings
let options: boolean[] = [true, false, false]; can only contain true or false
let books: object[] = [
  { name: 'Fooled by randomness', author: 'Nassim Taleb' },
  { name: 'Sapiens', author: 'Yuval Noah Harari' },
]; // can only contain objects
let arr: any[] = ['hello', 1, true]; // any basically reverts TypeScript back into JavaScript

ids.push(6);
ids.push('7'); // ERROR: Argument of type 'string' is not assignable to parameter of type 'number'.

你可以使用联合类型来定义包含多种类型的数组:

ts
let person: (string | number | boolean)[] = ['Danny', 1, true];
person[0] = 100;
person[1] = {name: 'Danny'} // Error - person array can't contain objects

如果你用一个值初始化一个变量,没有必要明确说明类型,因为TypeScript会推断出它的类型:

ts
let person = ['Danny', 1, true]; // This is identical to above example
person[0] = 100;
person[1] = { name: 'Danny' }; // Error - person array can't contain objects

一种特殊类型的数组可以在TypeScript中定义。元组(Tuples)。元组是一个具有固定大小和已知数据类型的数组, 它们比普通数组更严格。

ts
let person: [string, number, boolean] = ['Danny', 1, true];
person[0] = 100; // Error - Value at index 0 can only be a string

ts中的对象

TypeScript中的对象必须有所有正确的属性和值类型:

ts
// Declare a variable called person with a specific object type annotation
let person: {
  name: string;
  location: string;
  isProgrammer: boolean;
};

// Assign person to an object with all the necessary properties and value types
person = {
  name: 'Danny',
  location: 'UK',
  isProgrammer: true,
};

person.isProgrammer = 'Yes'; // ERROR: should be a boolean


person = {
  name: 'John',
  location: 'US',
}; 
// ERROR: missing the isProgrammer property

当定义一个对象的签名时,你通常会使用一个 接口(interface)。如果我们需要检查多个对象是否具有相同的特定属性和价值类型,这一点很有用 :

ts
interface Person {
  name: string;
  location: string;
  isProgrammer: boolean;
}

let person1: Person = {
  name: 'Danny',
  location: 'UK',
  isProgrammer: true,
};

let person2: Person = {
  name: 'Sarah',
  location: 'Germany',
  isProgrammer: false,
};

我们也可以用函数签名来声明函数属性。我们可以使用老式的普通JavaScript function(sayHi),或者ES6的箭头函数(sayBye)来做这件事:

ts
interface Speech {
  sayHi(name: string): string;
  sayBye: (name: string) => string;
}

let sayStuff: Speech = {
  sayHi: function (name: string) {
    return `Hi ${name}`;
  },
  sayBye: (name: string) => `Bye ${name}`,
};

console.log(sayStuff.sayHi('Heisenberg')); // Hi Heisenberg
console.log(sayStuff.sayBye('Heisenberg')); // Bye Heisenberg

注意在sayStuff对象中,sayHi或sayBye可以被赋予一个箭头函数或一个普通的JavaScript函数--TypeScript并不关心。

TypeScript中的函数

我们可以定义函数参数的类型,以及函数的返回类型:

ts
// Define a function called circle that takes a diam variable of type number, and returns a string
function circle(diam: number): string {
  return 'The circumference is ' + Math.PI * diam;
}

console.log(circle(10)); // The circumference is 31.41592653589793

同样的函数,但使用ES6箭头函数:

ts
const circle = (diam: number): string => {
  return 'The circumference is ' + Math.PI * diam;
};

console.log(circle(10)); // The circumference is 31.41592653589793

请注意,没有必要明确说明`circle'是一个函数;TypeScript会推断它。TypeScript也会推断出函数的返回类型,所以也不需要说明。不过,如果函数很大,有些开发者喜欢明确说明返回类型,以使其清晰明了。

ts
// Using explicit typing 
const circle: Function = (diam: number): string => {
  return 'The circumference is ' + Math.PI * diam;
};

// Inferred typing - TypeScript sees that circle is a function that always returns a string, so no need to explicitly state it
const circle = (diam: number) => {
  return 'The circumference is ' + Math.PI * diam;
};

我们可以在一个参数后面加一个问号,使其成为可选参数,不一定需要传入此参数。还注意到下面c是一个联合类型,可以是数字或字符串:

ts
const add = (a: number, b: number, c?: number | string) => {
  console.log(c);
  return a + b;
};

console.log(add(5, 4, 'I could pass a number, string, or nothing here!'));
// I could pass a number, string, or nothing here!
// 9

一个不返回任何东西的函数被说成是返回void--完全没有任何返回值。下面,已经明确说明了void的返回类型。但是,这也是没有必要的,因为TypeScript会推断出它。

ts
const logMessage = (msg: string): void => {
  console.log('This is the message: ' + msg);
};

logMessage('TypeScript is superb'); // This is the message: TypeScript is superb

动态(任意)类型

使用any类型,我们基本上可以将TypeScript还原成JavaScript:

ts
let age: any = '100';
age = 100;
age = {
  years: 100,
  months: 2,
};

建议尽量避免使用 "any "类型,因为它妨碍了TypeScript的工作--并可能导致错误。

类型别名

类型别名可以减少代码的重复,使我们的代码保持整洁。下面,我们可以看到PersonObject类型别名已经防止了重复,并且作为一个单一的真理来源,说明一个人的对象应该包含哪些数据。

ts
type StringOrNumber = string | number;

type PersonObject = {
  name: string;
  id: StringOrNumber;
};

const person1: PersonObject = {
  name: 'John',
  id: 1,
};

const person2: PersonObject = {
  name: 'Delia',
  id: 2,
};

const sayHello = (person: PersonObject) => {
  return 'Hi ' + person.name;
};

const sayGoodbye = (person: PersonObject) => {
  return 'Seeya ' + person.name;
};

DOM和类型转换

TypeScript并不像JavaScript那样可以访问DOM。这意味着,每当我们试图访问DOM元素时,TypeScript从不确定它们是否真的存在。

ts
const link = document.querySelector('a');

console.log(link.href); // ERROR: Object is possibly 'null'. TypeScript can't be sure the anchor tag exists, as it can't access the DOM

通过非空断言操作符(!),我们可以明确地告诉编译器,一个表达式的值不是 "空 "或 "未定义"。当编译器不能确定地推断出类型,但我们比编译器拥有更多的信息时,这就很有用。

ts
//  在这里,我们告诉TypeScript,我们确定这个锚点标签存在
const link = document.querySelector('a')!;

console.log(link.href); // www.freeCodeCamp.org

请注意,我们不必说明link变量的类型。这是因为TypeScript可以清楚地看到(通过类型推理)它是HTMLAnchorElement类型。

但是,如果我们需要通过它的类或id来选择一个DOM元素呢? TypeScript不能推断出类型,因为它可能是任何东西。

ts
const form = document.getElementById('signup-form');

console.log(form.method);
// ERROR: Object is possibly 'null'.
// ERROR: Property 'method' does not exist on type 'HTMLElement'.

以上,我们得到了两个错误。我们需要告诉TypeScript,我们确定form存在,而且我们知道它是HTMLFormElement的类型。我们通过类型转换来实现这一点:

ts
const form = document.getElementById('signup-form') as HTMLFormElement;

console.log(form.method); // post

TypeScript也有一个内置的事件对象。所以,如果我们在表单中添加一个提交事件监听器,如果我们调用任何不属于Event对象的方法,TypeScript会给我们一个错误。

ts
const form = document.getElementById('signup-form') as HTMLFormElement;

form.addEventListener('submit', (e: Event) => {
  e.preventDefault(); // prevents the page from refreshing

  console.log(e.tarrget); // ERROR: Property 'tarrget' does not exist on type 'Event'. Did you mean 'target'?
});

TypeScript中的类

我们可以在一个类中定义每一块数据具体的类型:

ts
class Person {
  name: string;
  isCool: boolean;
  pets: number;

  constructor(n: string, c: boolean, p: number) {
    this.name = n;
    this.isCool = c;
    this.pets = p;
  }

  sayHello() {
    return `Hi, my name is ${this.name} and I have ${this.pets} pets`;
  }
}

const person1 = new Person('Danny', false, 1);
const person2 = new Person('Sarah', 'yes', 6); // ERROR: Argument of type 'string' is not assignable to parameter of type 'boolean'.

console.log(person1.sayHello()); // Hi, my name is Danny and I have 1 pets

然后我们可以创建一个people数组,其中只包括由Person类构建的对象:

ts
let People: Person[] = [person1, person2];

我们可以在类的属性中添加访问修饰语。TypeScript也提供了一个新的访问修饰符,叫做只读(readonly)。

ts
class Person {
  readonly name: string; // This property is immutable - it can only be read
  private isCool: boolean; // Can only access or modify from methods within this class
  protected email: string; // Can access or modify from this class and subclasses
  public pets: number; // Can access or modify from anywhere - including outside the class

  constructor(n: string, c: boolean, e: string, p: number) {
    this.name = n;
    this.isCool = c;
    this.email = e;
    this.pets = p;
  }

  sayMyName() {
    console.log(`Your not Heisenberg, you're ${this.name}`);
  }
}

const person1 = new Person('Danny', false, 'dan@e.com', 1);
console.log(person1.name); // Fine
person1.name = 'James'; // Error: read only
console.log(person1.isCool); // Error: private property - only accessible within Person class
console.log(person1.email); // Error: protected property - only accessible within Person class and its subclasses
console.log(person1.pets); // Public property - so no problem

我们可以通过这样构建类(constructing class)的属性来使我们的代码更加简洁:

ts
class Person {
  constructor(
    readonly name: string,
    private isCool: boolean,
    protected email: string,
    public pets: number
  ) {}

  sayMyName() {
    console.log(`Your not Heisenberg, you're ${this.name}`);
  }
}

const person1 = new Person('Danny', false, 'dan@e.com', 1);
console.log(person1.name); // Danny

以上述方式编写,属性会在构造函数中自动分配--省去了我们把它们全部写出来的麻烦。

注意,如果我们省略了访问修饰符,默认情况下,该属性将是公共的(public)。

类也可以被扩展(extend),就像在普通的JavaScript中一样。

ts
class Programmer extends Person {
  programmingLanguages: string[];

  constructor(
    name: string,
    isCool: boolean,
    email: string,
    pets: number,
    pL: string[]
  ) {
    // The super call must supply all parameters for base (Person) class, as the constructor is not inherited.
    super(name, isCool, email, pets);
    this.programmingLanguages = pL;
  }
}

TypeScript中的模块

在JavaScript中,一个模块只是一个包含相关代码的文件。功能可以在模块之间被导入和导出,使代码保持良好的组织。

TypeScript也支持模块。TypeScript文件将被编译成多个JavaScript文件。

在tsconfig.json文件中,改变以下选项以支持现代的导入和导出:

json
{
  "target": "es2016",
  "module": "es2015"
}

TypeScript中的接口

接口定义了一个对象:

ts
interface Person {
  name: string;
  age: number;
}

function sayHi(person: Person) {
  console.log(`Hi ${person.name}`);
}

sayHi({
  name: 'John',
  age: 48,
}); // Hi John

你也可以使用一个类型别名来定义一个对象类型:

ts
type Person = {
  name: string;
  age: number;
};

function sayHi(person: Person) {
  console.log(`Hi ${person.name}`);
}

sayHi({
  name: 'John',
  age: 48,
}); // Hi John

或者可以匿名地定义一个对象类型:

ts
function sayHi(person: { name: string; age: number }) {
  console.log(`Hi ${person.name}`);
}

sayHi({
  name: 'John',
  age: 48,
}); // Hi John

接口与类型别名非常相似,在许多情况下,你可以使用这两者。关键的区别是,类型别名不能被重新打开以添加新的属性,而接口总是可以扩展的。

ts
interface Animal {
  name: string
}

interface Bear extends Animal {
  honey: boolean
}

const bear: Bear = {
  name: "Winnie",
  honey: true,
}

通过交集扩展一个类型:

ts
type Animal = {
  name: string
}

type Bear = Animal & {
  honey: boolean
}

const bear: Bear = {
  name: "Winnie",
  honey: true,
}

在现有接口上添加新字段:

ts
interface Animal {
  name: string
}

// Re-opening the Animal interface to add a new field
interface Animal {
  tail: boolean
}

const dog: Animal = {
  name: "Bruce",
  tail: true,
}

这里有一个关键的区别:一个类型在被创建后不能被改变。

ts
type Animal = {
  name: string
}

type Animal = {
  tail: boolean
}
// ERROR: Duplicate identifier 'Animal'.

TypeScript文档推荐使用接口来定义对象,直到你需要使用一个类型的功能。

接口也可以定义函数签名:

ts
interface Person {
  name: string
  age: number
  speak(sentence: string): void
}

const person1: Person = {
  name: "John",
  age: 48,
  speak: sentence => console.log(sentence),
}

使用接口的一个好处是,它只被TypeScript使用,而不是JavaScript。这意味着它不会被编译,也不会给你的JavaScript增加臃肿。类是JavaScript的特性,所以它会被编译。

另外,类本质上是一个对象工厂(也就是说,一个对象应该是什么样子的蓝图(blueprint),然后实现),而接口是一个仅用于类型检查的结构。

虽然一个类可能有初始化的属性和方法来帮助创建对象,但一个接口基本上定义了一个对象可以拥有的属性和类型。

有类的接口

我们可以通过实现一个接口来告诉一个类,它必须包含某些属性和方法:

ts
interface HasFormatter {
  format(): string;
}

class Person implements HasFormatter {
  constructor(public username: string, protected password: string) {}

  format() {
    return this.username.toLocaleLowerCase();
  }
}

// Must be objects that implement the HasFormatter interface
let person1: HasFormatter;
let person2: HasFormatter;

person1 = new Person('Danny', 'password123');
person2 = new Person('Jane', 'TypeScripter1990');

console.log(person1.format()); // danny

确保people是一个实现HasFormatter的对象数组(确保每个人都有一样的 format method):

ts
let people: HasFormatter[] = [];
people.push(person1);
people.push(person2);

TypeScript中的字面类型

除了一般的类型stringnumber之外,我们还可以在类型位置上引用特定的字符串和数字:

ts
// Union type with a literal type in each position
let favouriteColor: 'red' | 'blue' | 'green' | 'yellow';

favouriteColor = 'blue';
favouriteColor = 'crimson'; // ERROR: Type '"crimson"' is not assignable to type '"red" | "blue" | "green" | "yellow"'.

泛型

泛型允许你创建一个可以在多种类型上工作的组件,而不是单一的类型,这有助于使组件更容易重复使用。 addID "函数接受任何对象,并返回一个新的对象,该对象具有所有的属性和值,加上一个 "id "属性,其值在0到1000之间。简而言之,它给任何对象一个ID。

ts
const addID = (obj: object) => {
  let id = Math.floor(Math.random() * 1000);

  return { ...obj, id };
};

let person1 = addID({ name: 'John', age: 40 });

console.log(person1.id); // 271
console.log(person1.name); // ERROR: Property 'name' does not exist on type '{ id: number; }'.

正如你所看到的,当我们试图访问name属性时,TypeScript给出了一个错误。这是因为当我们传入一个对象到addID时,我们没有指定这个对象应该有什么属性,所以TypeScript不知道这个对象有什么属性(它没有 "捕获 "它们)。所以,TypeScript知道返回的对象的唯一属性是`id'。

那么,我们怎样才能向addID传递任何对象,但仍然告诉TypeScript这个对象有哪些属性和值呢?我们可以使用一个 generic, 其中T被称为 type parameter :

ts
// <T> is just the convention - e.g. we could use <X> or <A>
const addID = <T>(obj: T) => {
  let id = Math.floor(Math.random() * 1000);

  return { ...obj, id };
};

这有什么作用?现在,当我们把一个对象传给addID时,我们已经告诉TypeScript捕捉类型--所以T变成我们传入的任何类型。addID现在会知道我们传入的对象有哪些属性。

但是,我们现在有一个问题:任何东西都可以被传入addID,TypeScript将捕获类型,并报告没有问题:

ts
let person1 = addID({ name: 'John', age: 40 });
let person2 = addID('Sally'); // Pass in a string - no problem

console.log(person1.id); // 271
console.log(person1.name); // John

console.log(person2.id);
console.log(person2.name); // ERROR: Property 'name' does not exist on type '"Sally" & { id: number; }'.

当我们传入一个字符串时,TypeScript没有发现问题。它只在我们试图访问name属性时报告了一个错误。因此,我们需要一个约束:我们需要告诉TypeScript只接受对象,通过使我们的通用类型T成为object的扩展。

ts
const addID = <T extends object>(obj: T) => {
  let id = Math.floor(Math.random() * 1000);

  return { ...obj, id };
};

let person1 = addID({ name: 'John', age: 40 });
let person2 = addID('Sally'); // ERROR: Argument of type 'string' is not assignable to parameter of type 'object'.

我们可以通过说对象参数应该有一个带有字符串值的名称属性来解决这个问题:

ts
const addID = <T extends { name: string }>(obj: T) => {
  let id = Math.floor(Math.random() * 1000);

  return { ...obj, id };
};

let person2 = addID(['Sally', 26]); // ERROR: argument should have a name property with string value

在参数和返回类型提前未知的情况下,泛型允许你在组件中实现类型安全。

在TypeScript中,当我们想描述两个值之间的对应关系时,可以使用泛型。 在上面的例子中,返回类型与输入类型相关。

使用一个扩展了接口的泛型,确保传入的每个参数都有一个长度属性:

ts
interface hasLength {
  length: number;
}

function logLength<T extends hasLength>(a: T) {
  console.log(a.length);
  return a;
}

let hello = 'Hello world';
logLength(hello); // 11

let howMany = 8;
logLength(howMany); // Error: numbers don't have length properties

我们也可以写一个函数,参数是一个元素数组,这些元素都有一个长度属性:

ts
interface hasLength {
  length: number;
}

function logLengths<T extends hasLength>(a: T[]) {
  a.forEach((element) => {
    console.log(element.length);
  });
}

let arr = [
  'This string has a length prop',
  ['This', 'arr', 'has', 'length'],
  { material: 'plastic', length: 30 },
];

logLengths(arr);
// 29
// 4
// 30

带有接口的泛型

当我们不知道一个对象中的某个值会是什么类型时,我们可以使用一个泛型来传递类型:

ts
// The type, T, will be passed in
interface Person<T> {
  name: string;
  age: number;
  documents: T;
}

// We have to pass in the type of `documents` - an array of strings in this case
const person1: Person<string[]> = {
  name: 'John',
  age: 48,
  documents: ['passport', 'bank statement', 'visa'],
};

// Again, we implement the `Person` interface, and pass in the type for documents - in this case a string
const person2: Person<string> = {
  name: 'Delia',
  age: 46,
  documents: 'passport, P45',
};

TypeScript中的枚举(Enums)

枚举(Enums)是TypeScript带给JavaScript的一个特殊功能。枚举允许我们定义或声明相关值的集合,可以是数字或字符串,作为一组命名的常量。

ts
enum ResourceType {
  BOOK,
  AUTHOR,
  FILM,
  DIRECTOR,
  PERSON,
}

console.log(ResourceType.BOOK); // 0
console.log(ResourceType.AUTHOR); // 1

// To start from 1
enum ResourceType {
  BOOK = 1,
  AUTHOR,
  FILM,
  DIRECTOR,
  PERSON,
}

console.log(ResourceType.BOOK); // 1
console.log(ResourceType.AUTHOR); // 2

默认情况下,枚举(enums)是基于数字的--它们将字符串值存储为数字。但是它们也可以是字符串:

ts
enum Direction {
  Up = 'Up',
  Right = 'Right',
  Down = 'Down',
  Left = 'Left',
}

console.log(Direction.Right); // Right
console.log(Direction.Down); // Down

当我们有一组相关的常量时,枚举(Enums)很有用。例如,与其在整个代码中使用非描述性的数字,枚举可以通过描述性的常量使代码更具可读性。

枚举(Enums)也可以防止bug,因为当你输入枚举的名称时,智能感知(intellisense)会弹出,并给出可以选择的可能选项列表。

TypeScript严格模式

建议在tsconfig.json文件中启用所有严格的类型检查操作。这将导致TypeScript报告更多的错误,但将有助于防止许多错误悄悄进入你的应用程序。

json
// tsconfig.json
{
  ...,
  "strict": true
}

没有隐含的any

在下面的函数中,TypeScript已经推断出参数a是any类型。正如你所看到的,当我们传入一个数字到这个函数,并试图通过打印name属性值时,没有报告错误。

ts
function logName(a) {
  // No error??
  console.log(a.name);
}

logName(97);

在打开 "noImplicitAny "选项的情况下,如果我们没有明确说明a的类型,TypeScript将立即标记一个错误:

ts
// ERROR: Parameter 'a' implicitly has an 'any' type.
function logName(a) {
  console.log(a.name);
}

严格的空值检查(null checks)

当 "strictNullChecks "选项为false时,没有启用,TypeScript会忽略 null和 undefined。这可能在运行时出现意外的错误。

当strictNullChecks设置为 true时,变量被定义为null和undefined类型,如果你把它们赋给一个期望有具体数值的变量(例如,string),你会得到一个类型错误(type error)。

ts
let whoSangThis: string = getSong();

const singles = [
  { song: 'touch of grey', artist: 'grateful dead' },
  { song: 'paint it black', artist: 'rolling stones' },
];

const single = singles.find((s) => s.song === whoSangThis);

console.log(single.artist);

上面,singles.find不能保证它能找到这song,但我们写的代码就好像它总是能找到。

通过设置strictNullChecks为true,TypeScript将引发一个错误,因为我们在尝试使用single之前没有保证它的存在:

ts
const getSong = () => {
  return 'song';
};

let whoSangThis: string = getSong();

const singles = [
  { song: 'touch of grey', artist: 'grateful dead' },
  { song: 'paint it black', artist: 'rolling stones' },
];

const single = singles.find((s) => s.song === whoSangThis);

console.log(single.artist); // ERROR: Object is possibly 'undefined'.

TypeScript基本上是在告诉我们在使用single之前要确保它的存在。我们需要先检查它是否为 null或 undefined:

ts
if (single) {
  console.log(single.artist); // rolling stones
}

TypeScript中的type narrowing

在TypeScript程序中,变量可以从一个不太精确的类型转移到一个更精确的类型。 这个过程称为类型缩小(type narrowing)。

下面是一个简单的例子,显示了当我们使用if语句和typeof时,TypeScript如何将不太具体的string | number的类型缩小到更具体的类型:

ts
function addAnother(val: string | number) {
  if (typeof val === 'string') {
    // TypeScript treats `val` as a string in this block, so we can use string methods on `val` and TypeScript won't shout at us
    return val.concat(' ' + val);
  }

  // TypeScript knows `val` is a number here
  return val + val;
}

console.log(addAnother('Woooo')); // Woooo Woooo
console.log(addAnother(20)); // 40

参考资料与更多内容:

  1. TS中文网
  2. 菜鸟教程 教程