TypeScript--终极初学者指南

324 阅读26分钟

TypeScript--终极初学者指南

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

但不要惊慌——如果您已经了解 JavaScript,您将能够快速掌握 TypeScript。

即使你不打算使用 TypeScript,学习它也会让你更好地理解 JavaScript——并使你成为更好的开发人员。

在本文中,您将学习:

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

我还制作了一份Typescript清单。这使得快速查找和修改概念/语法变得容易。

TypeScript 备忘单 PDF

什么是TypeScript?

TypeScript 是 JavaScript 的超集,这意味着它可以完成 JavaScript 所做的所有事情,而且还具有一些附加功能。

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

另一方面,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 Compiler (TSC) 将其编译为 JavaScript——我们将很快讨论这一点。

TypeScript 值得去使用吗?

为什么你应该使用 TypeScript

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

TypeScript 的缺点

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

但是,您必须花在编写更精确的代码和编译上的额外时间将比您代码中的错误少得多。

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

如果您已经了解 JavaScript,那么 TypeScript 将不会太难学。这是您的武器库中的一个很好的工具。

如何配置 TypeScript 项目

安装 Node 和 TypeScript 编译器

首先,确保你的电脑上全局安装了NodeJS;

然后通过运行以下命令在您的机器上全局安装 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 种原始数据类型:

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

原始类型是不可变的:它们不能被改变。重要的是不要将原始类型本身与分配了原始类型值的变量混淆。可以为变量重新分配一个新值,但不能以更改对象、数组和函数的方式更改现有值。

这是一个例子:

let name = 'Danny';
name.toLowerCase();
console.log(name); // Danny - the string method didn't mutate the stringlet 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,并且它们都包含原始数据,那么它们是完全相互独立的:

原始数据存储在唯一的内存位置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)

这不是引用类型的情况。引用类型是指存储对象的内存位置。

引用类型内存位置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 中可以定义一种特殊类型的数组:元组。元组是具有固定大小和已知数据类型的数组。 它们比常规数组更严格。

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)。如果我们需要检查多个对象是否具有相同的属性和值类型,这很有用

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对象中,sayHi或者sayBye可以给定一个箭头函数或一个常见的 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)类型

使用any类型,我们基本上可以将 TypeScript 恢复为 JavaScript:

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

建议尽可能避免使用该any类型,因为它会阻止 TypeScript 完成其工作 - 并可能导致错误。

类型别名(type)

类型别名可以减少代码重复,保持我们的代码 DRY。下面,我们可以看到PersonObject类型别名防止了重复,并充当了 person 对象应包含哪些数据的单一事实来源。

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

使用非空断言运算符 (!),我们可以明确地告诉编译器一个表达式的值不是nullor 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存在,并且我们知道它是 type HTMLFormElement。我们通过类型转换来做到这一点:

const form = document.getElementById('signup-form') as HTMLFormElement;
​
console.log(form.method); // post

TypeScript 还内置了一个 Event 对象。因此,如果我们在表单中添加一个提交事件侦听器,如果我们调用任何不属于 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 中的模块

在 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(确保每个人都有 format 方法):

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 problemconsole.log(person1.id); // 271
console.log(person1.name); // Johnconsole.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 problemconsole.log(person2.id); // 824
console.log(person2.name); // Error: Property 'name' does not exist on type '(string | number)[] & { id: number; }'.

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

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); // 11let 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); // 11let 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 中的枚举

枚举是 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

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

枚举还可以防止错误,因为当您键入枚举的名称时,智能感知会弹出并为您提供可以选择的可能选项列表。

TypeScript 严格模式

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

 // tsconfig.json
 "strict": true

让我们讨论一下严格模式所做的一些事情:没有隐式任何,以及严格的空检查。

没有任何提示

在下面的函数中,TypeScript 已经推断出参数aany类型的。如您所见,当我们向该函数传递一个数字,并尝试记录一个name属性时,没有报错。不好。

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

打开该noImplicitAny选项后,如果我们没有明确声明 的类型,TypeScript 将立即标记错误a

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

严格的空检查

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

strictNullChecks设置为 true,如果将它们分配给需要具体值的变量(例如,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不能保证它会找到这首歌——但我们已经编写了代码,就好像它总是会一样。

通过设置为 true,TypeScript 将引发错误,因为在尝试使用它之前strictNullChecks我们没有保证存在: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 程序中,变量可以从不太精确的类型移动到更精确的类型。 这个过程称为类型缩小。

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

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

扩展:在React中使用TypeScript

TypeScript 完全支持 React 和 JSX。这意味着我们可以将 TypeScript 与三个最常见的 React 框架一起使用:

例如,要使用 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(对于带有 React 的 TypeScript)扩展名的文件,并使用 TypeScript 编写我们的组件。然后这将编译成公共文件夹中的 JavaScript。

在React props中使用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. 如果我们没有提供必要的 props,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;

以下是我们可以作为props类型的一些示例:

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

在React hooks中使用TypeScript

useState()

我们可以使用尖括号来声明状态变量应该是什么类型。下面,如果我们省略尖括号,TypeScript 会推断这cash是一个数字。所以,如果想让它也为null,我们必须指定:

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 对象应该引用什么类型——下面我们给ref指定了类型HTMLInputElement

const Person: React.FC = () => {
  // Initialise .current property to null
  const inputRef = useRef<HTMLInputElement>(null);
​
  return (
    <div>
      <input type='text' ref={inputRef} />
    </div>
  );
};

有关 React with TypeScript 的更多信息,请查看这些很棒的 React-TypeScript 备忘单

谢谢阅读

希望这很有用。如果你做到了这里,你现在知道了 TypeScript 的主要基础知识,并且可以开始在你的项目中使用它。