学习TypeScript - 最终的初学者指南

77 阅读27分钟

在过去的几年里,TypeScript变得越来越流行,许多工作现在都要求开发人员了解TypeScript。

但不要惊慌--如果你已经了解JavaScript,你将能够迅速掌握TypeScript。

即使你不打算使用TypeScript,学习它也会让你对JavaScript有更好的理解--让你成为更好的开发者。

在这篇文章中,你将学习:

  • 什么是 TypeScript,为什么要学习它?
  • 如何使用 TypeScript 设置项目
  • 所有主要的TypeScript概念(类型、接口、泛型、类型转换等...)。
  • 如何在React中使用TypeScript

我还制作了一份TypeScript小抄PDF海报,将这篇文章总结为一页。这使得它很容易快速查找和修改概念/语法。

TypeScript cheat sheet PDF

TypeScript小抄PDF

什么是 TypeScript?

TypeScript 是 JavaScript 的超集,也就是说,它可以完成 JavaScript 所做的一切,但增加了一些功能。

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

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

// 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编译器(TSC)编译成JavaScript--我们很快会讨论这个问题。

TypeScript 是否值得使用?

为什么要使用 TypeScript

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

TypeScript的缺点

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

但是,你必须花更多的时间来写更精确的代码和编译,这将比你的代码中的错误少得多。

对于许多项目--尤其是大中型项目--TypeScript将为你节省大量的时间和麻烦。

如果你已经知道了JavaScript,TypeScript也不会太难学。它是你武库中的一个伟大工具。

如何设置TypeScript项目

安装Node和TypeScript编译器

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

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

npm i -g typescript

检查安装是否成功(如果成功,它将返回版本号):

tsc -v

如何编译TypeScript

打开你的文本编辑器,创建一个TypeScript文件(例如,index.ts)。

编写一些JavaScript或TypeScript:

let sport = 'football';

let id = 5;

现在我们可以用下面的命令将其编译成JavaScript:

tsc index

TSC将把代码编译成JavaScript,并在一个名为index.js的文件中输出:

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

如果你想指定输出文件的名称。

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

如果你想让TSC自动编译你的代码,只要你做了改动,就添加 "watch "标志。

tsc index.ts -w

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

例如,下面的内容会使TypeScript立即报告错误:

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

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

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

这是TypeScript的一个重要属性:它假设开发者知道更多。即使有一个TypeScript错误,它也不会妨碍你编译代码。它告诉你有一个错误,但你是否对它做任何事情取决于你。

如何设置 ts 配置文件

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

首先,创建 ts 配置文件。

tsc --init

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

这里有一些需要注意的选项(如果使用TypeScript的前端框架,大部分的东西都会帮你处理好):

{
    "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
}

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

tsc -w

注意:当输入文件在命令行中被指定时(例如,tsc index ),tsconfig.json 文件被忽略。

TypeScript中的类型

原始类型

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

  • 字符串
  • 数字
  • 大数
  • 布尔型
  • 未定义
  • 符号

基元是不可变的:它们不能被改变。重要的是,不要将基元本身与分配给基元值的变量相混淆。变量可被重新分配一个新值,但现有值不能像对象、数组和函数那样被改变。

这里有一个例子:

let name = 'Danny';
name.toLowerCase();
console.log(name); // Danny - the string method didn't mutate the string

let arr = [1, 3, 5, 7];
arr.pop();
console.log(arr); // [1, 3, 5] - the array method mutated the array

name = 'Anna' // Assignment gives the primitive a new (not a mutated) value

在JavaScript中,所有的原始值(除了null和undefined)都有对应的对象,这些对象包裹着原始值。这些包装对象是String、Number、BigInt、Boolean和Symbol。这些包装对象提供了允许原始值被操纵的方法。

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

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

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

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

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

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

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

参考类型

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

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

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

注意事项:原始类型与引用类型

对于那些从未研究过原始类型与引用类型的人来说,让我们来讨论一下其根本区别。

如果一个原始类型被分配给一个变量,我们可以把这个变量看作是 包含基元值。每个基元值都存储在内存中的一个独特位置。

如果我们有两个变量,x和y,并且它们都包含原始数据,那么它们是完全相互独立的。

Primitive data are stored in unique memory locations

X和Y都包含唯一独立的原始数据

let x = 2;
let y = 1;

x = y;
y = 100;
console.log(x); // 1 (even though y changed to 100, x is still 1)

而引用类型则不是这样的。引用类型指的是存储对象的一个内存位置。

Reference types memory locations

point1和point2包含一个指向对象存储地址的引用

let point1 = { x: 1, y: 1 };
let point2 = point1;

point1.y = 100;
console.log(point2.y); // 100 (point1 and point2 refer to the same memory address where the point object is stored)

这是对原始类型与引用类型的一个快速概述。如果你需要更深入的解释,请看这篇文章。原始类型与引用类型

TypeScript中的数组

在TypeScript中,你可以定义一个数组可以包含什么类型的数据。

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'.

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

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

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

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)。一个元组是一个具有固定大小和已知数据类型的数组。 它们比普通数组更严格。

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

TypeScript中的对象

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

// 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 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函数(sayHi),或者ES6的箭头函数(sayBye)来做这个。

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 对象中,sayHisayBye 可以被赋予一个箭头函数或一个普通的JavaScript函数 - TypeScript并不关心。

TypeScript中的函数

我们可以定义函数参数应该是什么类型,以及函数的返回类型。

// 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箭头函数。

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

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

请注意,没有必要明确说明circle 是一个函数;TypeScript会推断它。TypeScript还推断了函数的返回类型,所以也不需要说明。虽然,如果函数很大,一些开发者喜欢明确说明返回类型,以达到清晰的效果。

// 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 ,它是一个联合类型,可以是数字或字符串。

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会推断出它。

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

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

如果我们想声明一个函数变量,但不定义它(说清楚它的作用), 那么使用一个函数签名。 下面,函数sayHello ,必须跟在冒号后的签名后面。

// Declare the varible sayHello, and give it a function signature that takes a string and returns nothing.
let sayHello: (name: string) => void;

// Define the function, satisfying its signature
sayHello = (name) => {
  console.log('Hello ' + name);
};

sayHello('Danny'); // Hello Danny

动态(任何)类型

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

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

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

类型别名

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

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都无法确定它们是否真的存在。

下面的例子显示了这个问题。

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

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

// Here we are telling TypeScript that we are certain that this anchor tag exists
const link = document.querySelector('a')!;

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

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

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

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 。我们用类型转换来做这件事。

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

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

TypeScript很高兴!

TypeScript也有一个内置的事件对象。所以,如果我们在表单中添加一个提交事件监听器,如果我们调用任何不属于Event对象的方法,TypeScript会给我们一个错误。看看TypeScript有多酷--它可以告诉我们什么时候犯了拼写错误。

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中的类

我们可以在一个类中定义每块数据应该有的类型。

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 类构建的对象。

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

我们可以为类的属性添加访问修改器。TypeScript还提供了一个新的访问修饰符,叫做readonly

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

我们可以通过这种方式构建类的属性,使我们的代码更加简洁。

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

以上述方式编写,属性会在构造函数中自动分配--使我们不必将它们全部写出来。

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

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

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官方文档

TypeScript中的模块

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

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

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

 "target": "es2016",
 "module": "es2015"

(虽然,对于Node项目,你很可能想要"module": "CommonJS" - Node还不支持现代的导入/导出。)

现在,在你的HTML文件中,将脚本导入改为模块类型。

<script type="module" src="/public/script.js"></script>

我们现在可以使用ES6导入和导出文件了。

// src/hello.ts
export function sayHi() {
  console.log('Hello there!');
}

// src/script.ts
import { sayHi } from './hello.js';

sayHi(); // Hello there!

注意:总是作为一个JavaScript文件导入,即使在TypeScript文件中。

TypeScript中的接口

接口定义了一个对象应该有的样子。

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

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

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

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

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

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

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

或者一个对象类型可以被匿名定义。

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

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

接口和类型别名非常相似,在很多情况下,你可以使用其中之一。关键的区别是,类型别名不能被重新打开以添加新的属性,而接口总是可以扩展的。

下面的例子来自TypeScript的文档

扩展一个接口。

interface Animal {
  name: string
}

interface Bear extends Animal {
  honey: boolean
}

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

通过交叉点扩展一个类型。

type Animal = {
  name: string
}

type Bear = Animal & {
  honey: boolean
}

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

向现有的接口添加新的字段。

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,
}

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

type Animal = {
  name: string
}

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

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

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

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的特性,所以它会被编译。

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

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

与类的接口

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

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 的对象数组(确保每个人都有格式方法)。

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

TypeScript中的字面类型

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

// 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。

 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这个对象有什么属性和值呢?我们可以使用一个通用的<T> - 其中T 被称为类型参数

// <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将捕获类型并报告没有问题。

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 的扩展。

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'.

这个错误被直接抓住了--完美......嗯,不完全是。在JavaScript中,数组就是对象,所以我们仍然可以通过传入数组的方式来解决。

let person2 = addID(['Sally', 26]); // Pass in an array - no problem

console.log(person2.id); // 824
console.log(person2.name); // Error: Property 'name' does not exist on type '(string | number)[] & { id: number; }'.

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

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

类型也可以传递给<T> ,如下所示--但这在大多数情况下是没有必要的,因为TypeScript会推断出它。

// Below, we have explicitly stated what type the argument should be between the angle brackets.
let person1 = addID<{ name: string; age: number }>({ name: 'John', age: 40 });

泛型允许你在参数和返回类型预先未知的组件中拥有类型安全。

在TypeScript中,当我们想描述两个值之间的对应关系时,就会使用泛型。在 上面的例子中,返回类型与输入类型有关。我们使用一个泛型 来描述这种对应关系。

另一个例子。如果我们需要一个接受多种类型的函数,使用泛型比使用any 型更好。下面显示了使用any 的问题。

function logLength(a: any) {
  console.log(a.length); // No error
  return a;
}

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

let howMany = 8;
logLength(howMany); // undefined (but no TypeScript error - surely we want TypeScript to tell us we've tried to access a length property on a number!)

我们可以尝试使用一个泛型。

function logLength<T>(a: T) {
  console.log(a.length); // ERROR: TypeScript isn't certain that `a` is a value with a length property
  return a;
}

至少我们现在得到了一些反馈,我们可以用它来收紧我们的代码。

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

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

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

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

泛型是TypeScript的一个很好的功能!

带有接口的泛型

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

// 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是TypeScript带给JavaScript的一个特殊功能。枚举允许我们定义或声明一组相关的值,可以是数字或字符串,作为一组命名的常量。

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

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

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

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

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

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

TypeScript严格模式

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

 // tsconfig.json
 "strict": true

让我们讨论一下严格模式所做的几件事:没有隐含的任何,和严格的空检查。

没有隐含的任何

在下面的函数中,TypeScript已经推断出参数aany 类型。正如你所看到的,当我们传入一个数字到这个函数,并试图记录一个name 属性时,没有报告错误。这不是好事。

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

logName(97);

随着noImplicitAny 选项的开启,如果我们没有明确说明a 的类型,TypeScript 会立即标记错误。

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

严格的空值检查

strictNullChecks 选项为假时,TypeScript有效地忽略了nullundefined 。这可能会导致在运行时出现意外的错误。

strictNullChecks 设置为 true 时,nullundefined 有自己的类型,如果你把它们赋值给一个期望有具体值的变量(例如,string ),你会得到一个类型错误。

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 并不能保证它能找到这首歌--但我们写的代码就好像它总是能找到。

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

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 存在。我们需要先检查它是否是nullundefined

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

TypeScript中的窄化

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

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

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

另一个例子:下面,我们定义了一个名为allVehicles 的联合类型,它可以是PlaneTrain 的类型。

interface Vehicle {
  topSpeed: number;
}

interface Train extends Vehicle {
  carriages: number;
}

interface Plane extends Vehicle {
  wingSpan: number;
}

type PlaneOrTrain = Plane | Train;

function getSpeedRatio(v: PlaneOrTrain) {
  // In here, we want to return topSpeed/carriages, or topSpeed/wingSpan
  console.log(v.carriages); // ERROR: 'carriages' doesn't exist on type 'Plane'
}

由于函数getSpeedRatio 是与多种类型一起工作的,我们需要一种方法来区分vPlane 还是Train 。我们可以通过给两种类型一个共同的区分属性,用一个字面字符串值来实现。

// All trains must now have a type property equal to 'Train'
interface Train extends Vehicle {
  type: 'Train';
  carriages: number;
}

// All trains must now have a type property equal to 'Plane'
interface Plane extends Vehicle {
  type: 'Plane';
  wingSpan: number;
}

type PlaneOrTrain = Plane | Train;

现在,我们和TypeScript可以缩小v 的类型。

function getSpeedRatio(v: PlaneOrTrain) {
  if (v.type === 'Train') {
    // TypeScript now knows that `v` is definitely a `Train`. It has narrowed down the type from the less specific `Plane | Train` type, into the more specific `Train` type
    return v.topSpeed / v.carriages;
  }

  // If it's not a Train, TypeScript narrows down that `v` must be a Plane - smart!
  return v.topSpeed / v.wingSpan;
}

let bigTrain: Train = {
  type: 'Train',
  topSpeed: 100,
  carriages: 20,
};

console.log(getSpeedRatio(bigTrain)); // 5

奖金:TypeScript与React

TypeScript 完全支持 React 和 JSX。这意味着我们可以在三个最常见的React框架中使用TypeScript。

如果你需要更多定制的React-TypeScript配置,你可以设置Webpack(一个模块捆绑器)并自己配置tsconfig.json 。但大多数时候,一个框架就可以完成这项工作。

例如,要用TypeScript设置create-react-app,只需运行。

npx create-react-app my-app --template typescript

# or

yarn create react-app my-app --template typescript

在src文件夹中,我们现在可以创建带有.ts (用于普通TypeScript文件)或.tsx (用于TypeScript with React)扩展的文件,并使用TypeScript编写我们的组件。然后,这将在public文件夹中编译成JavaScript。

React道具与TypeScript

下面,我们说Person 应该是一个React功能组件,接受一个props对象,propsname ,应该是一个字符串,age ,应该是一个数字。

// src/components/Person.tsx
import React from 'react';

const Person: React.FC<{
  name: string;
  age: number;
}> = ({ name, age }) => {
  return (
    <div>
      <div>{name}</div>
      <div>{age}</div>
    </div>
  );
};

export default Person;

但大多数开发者喜欢使用接口来指定道具类型。

interface Props {
  name: string;
  age: number;
}

const Person: React.FC<Props> = ({ name, age }) => {
  return (
    <div>
      <div>{name}</div>
      <div>{age}</div>
    </div>
  );
};

然后我们可以把这个组件导入到App.tsx 。如果我们没有提供必要的道具,TypeScript会给出一个错误。

import React from 'react';
import Person from './components/Person';

const App: React.FC = () => {
  return (
    <div>
      <Person name='John' age={48} />
    </div>
  );
};

export default App;

这里有几个例子说明我们可以有哪些道具类型。

interface PersonInfo {
  name: string;
  age: number;
}

interface Props {
  text: string;
  id: number;
  isVeryNice?: boolean;
  func: (name: string) => string;
  personInfo: PersonInfo;
}

React钩子与TypeScript

useState()

我们可以通过使用角括号声明一个状态变量应该是什么类型。下面,如果我们省略了角括号,TypeScript会推断出cash 是一个数字。所以,如果想让它也能成为空值,我们必须要指定。

const Person: React.FC<Props> = ({ name, age }) => {
  const [cash, setCash] = useState<number | null>(1);

  setCash(null);

  return (
    <div>
      <div>{name}</div>
      <div>{age}</div>
    </div>
  );
};

useRef()

useRef 返回一个可变的对象,在组件的生命周期内持续存在。我们可以告诉TypeScript这个ref对象应该指向什么--下面我们说这个prop应该是一个 。HTMLInputElement

const Person: React.FC = () => {
  // Initialise .current property to null
  const inputRef = useRef<HTMLInputElement>(null);

  return (
    <div>
      <input type='text' ref={inputRef} />
    </div>
  );
};

更多关于React与TypeScript的信息,请查看这些很棒的React-TypeScript小抄