为 JavaScript 开发人员提供的 Typescript 的内容、原因和方法

74 阅读9分钟

为JavaScript开发者提供的Typescript的内容、原因和方法

如果你是一个Javascript开发者,你一定或多或少地听说过Typescript。如果你一直不愿意尝试Typescript,因为你不确定它如何能比Javascript更好地服务于你,那么你就来对地方了。

本指南对Typescript做了一个介绍性的但全面的指导,任何Javascript开发者都需要开始使用它。

什么是Typescript,它的类型系统是什么,以及作为一个Javascript开发者,在你的下一个项目中使用Typescript会有什么好处?在本文的最后,你会找到所有这些问题的答案。

注意:我可能对Typescript有点偏见。在我开始的项目中,没有哪个项目是我喜欢JS而不是Typescript的。


什么是Typescript?

你可以把Typescript看成是一种在Javascript之上提供额外层的语言。

为什么呢?

虽然我们最初是用Typescript编写代码,但我们不能像运行Javascript那样直接在浏览器上运行Typescript。相反,Typescript要经过一个额外的编译步骤,将其代码转换为浏览器认可的Javascript。

因此,即使我们用Typescript编程,在浏览器上运行的最终程序也是Javascript。

那么,我们为什么要使用Typescript呢?

尽管Typescript在运行时并没有提供比Javascript更多的功能,但它提供了一系列的功能,以确保我们这些开发者能够写出比使用Javascript更少的错误和更好的可维护代码。

Typescript是如何做到这一点的?

顾名思义,Typescript在普通的Javascript之上引入了一个类型系统。在Javascript中,变量的类型是动态分配的,而Typescript迫使我们预先定义我们要声明的变量的类型。

在Javascript中,我们可以在第一行给一个变量分配一个整数值,然后在下一行给它分配一个字符串值:

let jsVar = 0;
jsVar = "js";

但是在Typescript中,我们可以通过明确声明变量的类型来限制这种行为。如果我们试图给一个 "数字 "类型的变量分配一个字符串,就会产生一个错误:

let tsVar: number = 0;
tsVar = "ts"; //error

VS代码对错误的类型分配发出警告

简而言之,这就是Typescript与Javascript的不同之处:用类型来防止我们在代码中犯愚蠢的错误。


类型脚本是如何改进Javascript的

虽然缺乏定义类型的能力不一定是Javascript的缺陷,但它给了程序员太多的自由,这不可避免地导致他们写出糟糕的代码:

let aNumber = 123;

aNumber = {
    name: "John",
    age: 23
}

在上述Javascript的情况下,没有什么可以阻止开发者使用aNumber 变量来表示一个对象。虽然这不是一个会让程序崩溃的错误,但它破坏了使用变量名来自我记录代码的目的。

Typescript很容易解决这个问题,它在声明时定义了变量的类型,这样它就不能被分配给另一种类型的值:

let aNumber: number = 123;

如果其他开发者能够访问你程序中的这个变量,他们现在可以相信它的值是一个数字,就像它的名字所暗示的那样:

function isEligible(personObj) {
    return personObj.age > 34;
}

let john = {
    name: "John",
    age: 23
};

isEligible(john);

在这个例子中,isEligible 函数期望有一个对象,它有一个名为age的字段。但是Javascript没有办法保证传递给函数的参数实际上是一个对象,或者它有一个名为age的字段。

同样,Typescript有办法解决这个问题:

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

function isEligible(personObj: Person) {
    return personObj.age;
}

let john = {
    name: "John",
    age: 23
};

isEligible(john);

现在,这段代码对你来说可能没有意义。但请注意它是如何确保传递的变量的类型是Person类型,这是在开始时定义的。

使用Typescript将从你的程序中删除数百个粗心的编码错误,并防止你在每次遇到最愚蠢的bug时不得不拔出头发。它还会使你的代码更好地自我记录,提高其可维护性。

如果你对IDE中为Javascript提供的代码建议不足感到沮丧,那么你就有另一个理由来尝试一下Typescript。类型的存在使Typescript有能力在IDE中显示更好的代码建议。


使用Typescript的类型

基本类型

Typescript有一些预定义的基本类型。数字、字符串、布尔值和数组是其中的几个例子。

你可以在Typescript文档中找到基本类型的完整列表。

这里有几个例子:

const num: number = 0;
const firstName: string = 'Juan';
const isValid: boolean = true;
const obj: object = {
    id: 1,
    name: 'Juan',
};

// Two ways to define arrays
const names: string[] = ['Juan', 'Sean', 'Jane'];
const dogs: Array<string> = ['Rex', 'Woof', 'Puppy'];

// any type variables can be of any type
let newVar: any = 'Hello World';
newVar = 89;
newVar = false;

// You can define the type as a union of several types
let numOrBoolean: number | boolean = 12;
numOrBoolean = true;
numOrBoolean = "hey"; // error

请注意,任何类型都会使Typescript恢复到与Javascript相同的行为方式。由于我们使用Typescript的目的是为了给我们的代码提供一个更好的结构,所以尽可能避免使用任何类型。

同样地,尽量避免使用类型的联合,但如果不可避免,应尽可能地限制联合中允许的类型数量。

声明自定义类型

还记得我在之前的代码例子中是如何使用一个叫做Person的类型吗?但是Person在Typescript中不是一个基本的数据类型。我根据自己的要求创建了Person类型,将其作为给定函数接受的参数类型。

我们使用接口来定义我们要引入到应用程序中的新类型的基本结构:

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

现在,如果我们创建一个类型为Person 的新对象,它里面应该有姓名和年龄两个字。如果没有,Typescript会抛出一个错误。

VS代码对自定义类型中丢失的属性发出警告

你也可以在一个接口中定义可选字段:

interface Address {
    houseNumber: number,
    street: string,
    city: string,
    country?: string
}

const address1: Address = {
    houseNumber: 134,
    street: "Down road",
    city: "Berlin",
    country: "Germany"
}

const address2: Address = {
    houseNumber: 2254,
    street: "Up road",
    city: "London",
}

然后你可以在定义另一个类型时使用一个自定义类型作为字段的类型。

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

扩展接口

在Typescript中,你可以通过扩展另一个类型的接口来继承它的属性。

假设你的应用程序需要两种不同的类型,Person和Employee。由于雇员也是一个人,所以在创建Employee接口时,继承person类型的属性是有意义的。它可以防止代码重复。

你可以通过扩展Person接口快速实现这一点:

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

interface Employee extends Person {
    jobName: string;
    salary: number;
}

const employeeJohn: Employee = {
    name: "John",
    age: 34,
    jobName: "Javascript developer",
    salary: 54000
}

函数参数类型和返回类型

与变量类型类似,你可以为函数参数和返回值定义类型。参数类型在参数名旁边声明,而返回类型则在大括号之前声明。

interface Car {
    id: number;
    color: string;
    sold: boolean;
}

function getSoldCarCount(cars: Array<Car>) : number {
    return cars.reduce<number>((acc, car) => acc + car.sold ? 1 : 0, 0);
}

const car1: Car = {
    id: 23,
    color: "red",
    sold: false
}

const car2:Car = {
    id: 78,
    color: "black",
    sold: true
}

const car3: Car = {
    id: 12,
    color: "yellow",
    sold: true
}

const cars: Array<Car> = [car1, car2, car3]

let soldCarCount = getSoldCarCount(cars);

在定义了参数和返回值的类型后,我们可以保证你或其他使用这个函数的人不会意外地传递一个不具备汽车类型特征的对象。

你也可以保证任何传递的对象中的字段sold ,不会是未定义或空的。而且它消除了一些可能在运行时抛出错误的情况。如果你使用Javascript,你将不得不写更多的代码来防止在运行时发生这种错误的可能性。

与变量类似,你可以将返回和参数类型定义为几种类型的联合:

function buyCar(car : Car): Car | boolean {
    if (car.sold === true){
        return false;
    }
    return car;
}

当你声明接受的参数或返回类型时,扩展初始类型的接口的类型的对象也被接受为参数或返回值:

enum Manufacturer {
    Fiat,
    Porsche,
    Audi,
    BMW
}

interface ImportedCar extends Car {
    manufacturer: Manufacturer
}

const newImportedCar: ImportedCar = {
    id: 456,
    color: "black",
    sold: false,
    manufacturer: Manufacturer.Fiat
}

buyCar(newImportedCar);

使用泛型

使用Typescript,你可以像我们到目前为止所收敛的那样,轻松地定义泛型变量。如果你正在定义一个泛型函数,你可以用它来处理属于任何内置或自定义类型的数据:

function getInfo<T>(input: T): T {
    return input;
}

const stringInfo = getInfo<string>("Hello World");
const numberInfo = getInfo<number>(3321);
const carInfo = getInfo<Car>(car1);

如果你使用任意类型而不是泛型呢?

当然,你可以使用any类型改变上述函数以接受任何类型的参数:

function getInfo(input : any) {
    return input;
}

const stringInfo: string = getInfo("Hello World");
const numberInfo: number = getInfo(3321);
const carInfo: Car = getInfo(car1);

然而,这种方法并不保留传递给函数的数据类型。相反,它将传递的每个参数都记录为属于任何类型。此外你应该避免使用any

不过,通过泛型,你可以保留传递给函数的数据类型。如果你想根据传递的数据类型来改变函数的逻辑,使用泛型比接受任何类型的数据要好。


使用类型别名

当你想在应用程序中使用的特定字段可能属于几种类型之一时,你可以将其类型定义为这些独立类型的联合:

function accept(input: string): string | boolean | number {
    if (input.length > 13){
        return false;
    } else if (input.split("/").length < 3){
        return input.split("/").join(".");
    } else {
        return input.split("/").length;
    }
}

function reject(input: string): string | boolean | number {
    if (input.length > 13){
        return true;
    } else if (input.split("/").length < 3){
        return input.split("/").join(".");
    } else {
        return input.split("/").length;
    }
}

你可以使用type关键字为union定义一个别名,而不是像上面那样,每次都要重写union:

type resultType = string | boolean | number;

function accept(input : string) : resultType {
    ...
}

function reject(input : string) : resultType {
    ...
}

现在,你不必再使用一个长的类型联盟了。另外,如果你想在将来对函数的返回类型进行修改,你现在只需要修改一行代码。


类型转换

当一个类型通过扩展另一个类型的接口来定义时,两者之间产生的关系使我们有权限将其中一个类型中定义的对象转换为另一个类型。

以我之前定义的Car和ImportedCar类型为例。首先,我将创建一个ImportedCar类型的对象,看看转换在它身上是如何进行的:

const newImportedCar: ImportedCar = {
    id: 456,
    color: "black",
    sold: false,
    manufacturer: "fiat"
}

const convertedCar = <Car>newImportedCar; //no error

//another syntax for the conversion
const anotherConvertedCar = newImportedCar as Car;

这段代码的编译没有错误。这种转换是有道理的,因为ImportedCar类型已经拥有了在Car类型中定义的所有字段。

如果我们试图访问转换前对象中定义的制造商字段,会产生一个错误,因为转换后的对象是Car类型的。

convertedCar.manufacturer;  //error

这种转换也可以反过来进行。我们可以将一个Car对象转换为ImportedCar对象:

const newCar: Car = {
    id : 234,
    color: "green",
    sold: true,
}

const convertedImportedCar = <ImportedCar> newCar;

在这种情况下,如果你试图访问Car对象以前没有的新字段,即制造商,你会看到它返回未定义:

convertedImportedCar.manufacturer;   //undefined

总结

我希望这篇文章能消除你对使用Typescript进行前端开发的任何疑虑。由于Typescript中的大部分功能已经与Javascript相似,你也能在短时间内掌握Typescript。这肯定会在你的下一个项目中得到回报。

接下来,你就会像我一样,成为一个离不开Typescript的Javascript开发者。

谢谢你的阅读!