阅读 5125

【TS 演化史 -- 破晓】一步一个脚印带你入门 TS

作者:Valentino Gagliardi

译者:前端小智

来源:valentinog


什么是 TypeScript

官方网站的定义是:TypeScript 是 JS 类型的超集。它假设咱们知道什么是超集,什么是类型化。为了简单起见,你可以将 TypeScript 看作是 JavaScript 之上的一个外壳

TypeScript 是一个外壳,因为编写 TypeScript 的代码,在编译之后,,剩下的只是简单的 JS 代码。

但是 JS 引擎无法读取 TypeScript 代码,因此任何 TypeScript 文件都应该经过预翻译过程,即编译。只有在第一个编译步骤之后,才剩下纯 JS 代码,可以在浏览器中运行。稍后会介绍 TypeScript 编译是如何完成的。

现在让我们记住 TypeScript 是一种特殊的 JS,在浏览器中运行之前它需要一个翻译。

为什么要使用 TypeScript

刚开始,咱们不完全理解 TypeScript 为何有意义。 你可能会问“ TypeScript 的目的是什么”。 这是一个很好的问题。

实际上,一旦它在您的代码中发现严重和愚蠢的错误,你就会看到 TypeScript 的好处。更重要的是,TypeScript 会让代码变得结构良好而且还是自动,这些还只是 TypeScript 的一小部分。

不管怎样,也经常有人说 TypeScript 没用,太过笨拙。

凡事都有两面性,TypeScript 有很多反对者和支持者,但重要的是 TypeScript 是一个可靠的工具,将它放在咱们的工具技能包中不会造成伤害。

TypeScript 配置

为什么配置? TypeScript 还有一个二进制文件,可将 TypeScript 代码编译为 JS 代码. 请记住,浏览器不理解 TypeScript:

mkdir typescript-tutorial && cd $_
npm init -y
复制代码

然后安装 TypeScript

npm i typescript --save-dev
复制代码

接下来在 package.json 中的 scripts 下添加如下内容,以便咱们可以轻松地运行 TypeScript 编译器:

  "scripts": {
    "tsc": "tsc"
  }
复制代码

tsc 代表 TypeScript 编译器,只要编译器运行,它将在项目文件夹中查找名为tsconfig.json 的文件。 使用以下命令为 TypeScript 生成配置文件:

npm run tsc -- --init
复制代码

执行成功后会在控制台收到 message TS6071: Successfully created a tsconfig.json file。在项目文件夹中会看到新增了一个 tsconfig.json 文件。tsconfig。json 是一个可怕的配置文件,不要慌。咱们不需要知道它的每一个要点,在下一节中,会介绍入门的相关部分。

配置TypeScript 编译器

最好先初始化 git repo 并提交原始的 tsconfig.json,然后再打开文件。 我们将只保留一些配置选项,并删除其他所有内容。 稍后,你可能需要将现在的版本与原始版本进行比较。

首先,请打开 tsconfig.json 并将所有原始内容替换为以下内容:

{
  "compilerOptions": {
    "target": "es5",
    "strict": true
  }
}
复制代码

保存并关闭文件。 首先,你可能想知道 tsconfig.json 是干什么的。 该配置文件由 TypeScript 编译器和任何具有 TypeScript 支持的代码编辑器读取。

  • noImplicitAny true:当变量没有定义类型时,TypeScript 会报错

  • alwaysStrict true:严格模式是 JS 的安全机制,它可以防止意外的全局变量,默认的 this 绑定等。 设置为 “alwaysStrict” 时,TypeScript 在每个KS 文件的顶部都使用 “use strict”

有更多的配置选项可用。随着时间的推移,你会学到更多,因为现在上面的两个选择是你开始学习时需要知道的一切。

关于类型的几个词

TypeScript 支持与 JS 几乎相同的数据类型,此外,TypeScript 自己添加了更多的类型,如 any 类型一样。

“any” 是松散的 TypeScript 类型。 这意味着:此变量可以是任何类型:字符串,布尔值,对象等。 实际上,这就像根本没有类型检查。

TypeScript 中的行为

咱们从一个合法的 KS函数开始:filterByTerm。在项目文件夹中创建一个名为 filterByTerm.js 的新文件,并输入以下内容

function filterByTerm(input, searchTerm) {
  if (!searchTerm) throw Error("searchTerm 不能为空");
  if (!input.length) throw Error("input 不能为空");
  const regex = new RegExp(searchTerm, "i");
  return input.filter(function(arrayElement) {
    return arrayElement.url.match(regex);
  });
}

filterByTerm("input string", "java");
复制代码

如果现在不了解逻辑,请不要担心。 看一下该函数的参数,以及几行之后如何使用它们。 只需查看代码,就应该已经发现了问题。

我想知道是否有一种方法可以在我的 IDE 中检查这个函数,而不需要运行代码或者用Jest测试它。这可能吗? TypeScript 在这方面做得很好,实际上它是 JS 中静态检查的最佳工具,也就是说,在代码运行之前测试代码的正确性

因此,咱们改用 TypeScript ,将文件的扩展名从 filterByTerm.js 改为 filterByTerm.ts。通过这种更改,你会发现代码中的一堆错误

可以看到函数参数下的有很多红色标记。从现在开始,会向你展示文本形式的错误,但是请记住,当咱们在TypeScript 中出错时,IDE 和文本编辑器都会显示这些红线。

确定哪个地方错:

npm run tsc
复制代码

可以看到控制的报错:

filterByTerm.ts:1:23 - error TS7006: Parameter 'input' implicitly has an 'any' type.

1 function filterByTerm(input, searchTerm) {
                        ~~~~~

filterByTerm.ts:1:30 - error TS7006: Parameter 'searchTerm' implicitly has an 'any' type.

1 function filterByTerm(input, searchTerm) {
                               ~~~~~~~~~~

filterByTerm.ts:5:32 - error TS7006: Parameter 'arrayElement' implicitly has an 'any' type.

5   return input.filter(function(arrayElement) {
复制代码

TypeScript 告诉你函数参数具有 “any” 类型,如果还记得的话,它可以是 TypeScript 中的 any 类型。 我们需要在我们的 TypeScript 代码中添加适当的类型注释。

什么是类型,JS 中有什么问题

到目前为止,JS 有七种类型

  • String
  • Number
  • Boolean
  • Null
  • Undefined
  • Object
  • Symbol (ES6)

除了 Object 类型外,其它是 JS 的基本数据类型。每种 JS 类型都有相应的表示,可以代码中使用,比如字符串和数字

var name = "Hello John";
var age = 33;
复制代码

JS 的问题是,变量可以随时更改其类型。例如,布尔值可以变成字符串(将以下代码保存到名为 types.js 的文件中)

var aBoolean = false;
console.log(typeof aBoolean); // "boolean"

aBoolean = "Tom";
console.log(typeof aBoolean); // "string"
复制代码

转换可以是有意的,开发人员可能真的希望将 Tom 分配到 aBoolean,但是这类错误很可能是偶然发生的。

从技术上讲,JS 本身没有什么问题,因为它的类型动态是故意的。JS 是作为一种简单的 web 脚本语言而诞生的,而不是作为一种成熟的企业语言。

然而,JS 松散的特性可能会在你的代码中造成严重的问题,破坏其可维护性。TypeScript 旨在通过向 JS 添加强类型来解决这些问题。实际上,如果将 types.js 的扩展更改为 types.ts 。你会在IDE中看到 TypeScript 的抱怨。

types.ts 的编译控制台会报错:

types.ts:4:1 - error TS2322: Type '"Tom"' is not assignable to type 'boolean'.
复制代码

有了这些知识,接着,咱们更深入地研究 TypeScript 类型。

深入 TypeScript 类型

TypeScript 强调有类型,咱们上面的代码根本没有类型,是时候添加一些了。首先要修正函数参数。通过观察这个函数是如何调用的,它似乎以两个字符串作为参数:

filterByTerm("input string", "java");
复制代码

为参数添加类型:

function filterByTerm(input: string, searchTerm: string) {
    // ...
}

// ...
复制代码

接着编译:

npm run tsc
复制代码

剩下的错误:

filterByTerm.ts:5:16 - error TS2339: Property 'filter' does not exist on type 'string'.
复制代码

可以看到 TypeScript 是如何指导我们,现在的问题在于 filter 方法。

function filterByTerm(input: string, searchTerm: string) {
  // 省略一些
  return input.filter(function(arrayElement) {
    return arrayElement.url.match(regex);
  });
}
复制代码

咱们告诉 TypeScript “input” 是一个字符串,但是在后面的代码中调用了 filter 方法,它属于数组。我们真正需要的是将输入标记为某个东西的数组,可能是字符串数组:

为此,有两个选择。选项1:string[]

function filterByTerm(input: string[], searchTerm: string) {
    // ...
}
复制代码

选项2: Array<Type>

function filterByTerm(input: Array<string>, searchTerm: string) {
    // ...

}
复制代码

我个人更喜欢选项2。 现在,尝试再次编译(npm run tsc),控制台信息如下:

filterByTerm.ts:10:14 - error TS2345: Argument of type '"input string"' is not assignable to parameter of type 'string[]'.

filterByTerm("input string", "java");
复制代码

TypeScript 还会校验传入的类型。 我们将 input 改为字符串数组:

filterByTerm(["string1", "string2", "string3"], "java");
复制代码

这是到目前为止的完整代码:

function filterByTerm(input: Array<string>, searchTerm: string) {
  if (!searchTerm) throw Error("searchTerm 不能为空");
  if (!input.length) throw Error("input 不能为空");
  const regex = new RegExp(searchTerm, "i");
  return input.filter(function(arrayElement) {
    return arrayElement.url.match(regex);
  });
}

filterByTerm(["string1", "string2", "string3"], "java");
复制代码

看上去很好,但是,编译(npm run tsc)还是过不了:

filterByTerm.ts:6:25 - error TS2339: Property 'url' does not exist on type 'string'.
复制代码

TypeScript 确实很严谨。 我们传入了一个字符串数组,但是在代码后面,尝试访问一个名为 “url” 的属性:

return arrayElement.url.match(regex);
复制代码

这意味着咱们需要一个对象数组,而不是字符串数组。咱们在下一节中解决这个问题。

TypeScript 对象和接口

上面遗留一个问题:因为 filterByTerm 被传递了一个字符串数组。url 属性在类型为 string 的TypeScript 上不存在。所以咱们改用传递一个对象数组来解决这个问题:

filterByTerm(
  [{ url: "string1" }, { url: "string2" }, { url: "string3" }],
  "java"
);
复制代码

函数定义也要对应的更改:

function filterByTerm(input: Array<object>, searchTerm: string) {
    // omitted
}
复制代码

现在让我们编译代码

npm run tsc
复制代码

控制输出:

filterByTerm.ts:6:25 - error TS2339: Property 'url' does not exist on type 'object'.
复制代码

又来了,通用 JS 对象没有任何名为 url 的属性。对我来说,TypeScript 对类型要求真的是很严谨。

这里的问题是,咱们不能给一个随机对象分配属性,TypeScript 的核心原则之一是对值所具有的结构进行类型检查, 在 TypeScript 里,接口(interface)的作用就是为这些类型命名和为你的代码或第三方代码定义契约,咱们可以使用接口来解决这个问题。

通过查看我们的代码,我们可以想到一个名为 Link 的简单**"模型"**,其结构应该符合以下模式:它必须有一个类型为 stringurl 属性。

在TypeScript 中,你可以用一个接口来定义这个模型,就像这样(把下面的代码放在 filterByTerm.ts 的顶部):

interface ILink {
  url: string;
}
复制代码

对于接口声明,这当然不是有效的 JS 语法,在编译过程中会被删除。

提示:在定义接口名字前面加上大写的I,这是 TypeScript 的惯例。

现在,使用使用接口 ILink 定义 input 类型

function filterByTerm(input: Array<ILink>, searchTerm: string) {
    // ...
}
复制代码

通过此修复,可以说 TypeScript “期望 ILink 数组”作为该函数的输入,以下是完整的代码:

interface ILink {
  url: string;
}

function filterByTerm(input: Array<ILink>, searchTerm: string) {
  if (!searchTerm) throw Error("searchTerm 不能为空");
  if (!input.length) throw Error("input 不能为空");
  const regex = new RegExp(searchTerm, "i");
  return input.filter(function(arrayElement) {
    return arrayElement.url.match(regex);
  });
}

filterByTerm(
  [{ url: "string1" }, { url: "string2" }, { url: "string3" }],
  "java"
);
复制代码

此时,所有的错误都应该消失了,可以运行了:

npm run tsc
复制代码

编译后,会在项目文件夹中生成一个名为 filterByTerm.js 的文件,其中包含纯 JS 代码。可以检出该文件并查看 TypeScript 特定的声明最终转换成 JS 的是什么样的。

因为 alwaysStrict 设置为 true,所以 TypeScript 编译器也会在 filterByTerm.js 的顶部使用 use strict

接口和字段

TypeScript 接口是该语言最强大的结构之一。接口有助于在整个应用程序中形成模型,这样任何开发人员在编写代码时都可以选择这种模型并遵循它。

前面,咱们定义了一个简单的接口 ILink

interface ILink {
  url: string;
}
复制代码

如果您想要向接口添加更多的字段,只需在块中声明它们即可:

interface ILink {
  description: string;
  id: number;
  url: string;
}
复制代码

现在,类型为ILink的对象都必须实现新字段,否则就会出现错误,如果把上面 的定义重新写入 filterByTerm.ts 然后重新编译就会报错了:

filterByTerm.ts:17:4 - error TS2739: Type '{ url: string; }' is missing the following properties from type 'ILink': description, id
复制代码

问题在于我们函数的参数:

filterByTerm(
  [{ url: "string1" }, { url: "string2" }, { url: "string3" }],
  "java"
);
复制代码

TypeScript 可以通过函数声明来推断参数是 ILink 的类型数组。因此,该数组中的任何对象都必须实现接口 ILink 中定义的所有字段

大多数情况下,实现所有字段是不太现实的。毕竟,咱也不知道 ILink 类型的每个新对象是否会需要拥有所有字段。不过不要担心,要使编译通过,可以声明接口的字段可选,使用 ? 表示:

interface ILink {
  description?: string;
  id?: number;
  url: string;
}
复制代码

现在编辑器和编译器都没问题了。然而 TypeScript 接口可以做的更多,在下一节我们将看到如何扩展它们。但首先简要介绍一下 TypeScript 中的变量。

变量声明

到目前为止,咱们已经了解了如何向函数参数中添加类型:

function filterByTerm(input: Array<ILink>, searchTerm: string) {
    //
}
复制代码

TypeScript 并不限于此,当然也可以向任何变量添加类型。为了说明这个例子,咱们一一地提取函数的参数。首先咱要提取每一个单独的对象:

const obj1: ILink = { url: "string1" };
const obj2: ILink = { url: "string2" };
const obj3: ILink = { url: "string3" };
复制代码

接下来我们可以像这样定义一个 ILink 数组:

const arrOfLinks: Array<ILink> = [obj1, obj2, obj3];
复制代码

参数 searchTerm 对应的类型可以这样:

const term: string = "java";
复制代码

以下是完整的代码:

interface ILink {
  description?: string;
  id?: number;
  url: string;
}

function filterByTerm(input: Array<ILink>, searchTerm: string) {
  if (!searchTerm) throw Error("searchTerm 不能为空");
  if (!input.length) throw Error("input 不能为空");
  const regex = new RegExp(searchTerm, "i");
  return input.filter(function(arrayElement) {
    return arrayElement.url.match(regex);
  });
}

const obj1: ILink = { url: "string1" };
const obj2: ILink = { url: "string2" };
const obj3: ILink = { url: "string3" };

const arrOfLinks: Array<ILink> = [obj1, obj2, obj3];

const term: string = "java";

filterByTerm(arrOfLinks, term);
复制代码

与 JS 相比,TypeScript看起来更冗长,有时甚至是多余的。但是随着时间的推移,会发现添加的类型越多,代码就越健壮。

通过添加类型注释,对 TypeScript 的了解也越多,还可以帮助你更好地理解代码的意图。

例如,arrOfLinks 与正确的类型(ILink的数组)相关联,咱们编辑器就可以推断出数组中的每个对象都有一个名为url的属性,如接口 ILink 中所定义:

除了字符串、数组和数字之外,TypeScript 还有更多类型。有 booleantuple (元组)any, neverenum 。如果你感兴趣,可以查看文档

现在,咱们继续扩展接口。

扩展接口

TypeScript 接口很好。但是,如果哪天咱们需要一个新的对象,所需的类型跟现在有接口基本差不多。假设我们需要一个名为 IPost 的新接口,它具有以下属性:

  • id, number
  • title, string
  • body, string
  • url, string
  • description, string

该接口的字段其中有些,我们 ILink 接口都有了。

interface ILink {
  description?: string;
  id?: number;
  url: string;
}
复制代码

是否有办法重用接口 ILink ? 在 TypeScript 中,可以使用继承来扩展接口,关键字用 extends 表示:

interface ILink {
  description?: string;
  id?: number;
  url: string;
}

interface IPost extends ILink {
  title: string;
  body: string;
}
复制代码

现在,IPost 类型的对象都将具有可选的属性 descriptionidurl和必填的属性 titlebody

interface ILink {
  description?: string;
  id?: number;
  url: string;
}

interface IPost extends ILink {
  title: string;
  body: string;
}

const post1: IPost = {
  description:
    "TypeScript tutorial for beginners is a tutorial for all the JavaScript developers ...",
  id: 1,
  url: "www.valentinog.com/typescript/",
  title: "TypeScript tutorial for beginners",
  body: "Some stuff here!"
};
复制代码

当像 post1 这样的对象使用一个接口时,我们说 post1 实现了该接口中定义的属性。

扩展接口意味着借用其属性并扩展它们以实现代码重用。当然 TypeScript 接口还也可以描述函数,稍后会看到。

索引

JS 对象是键/值对的容器。 如下有一个简单的对象:

const paolo = {
  name: "Paolo",
  city: "Siena",
  age: 44
};
复制代码

我们可以使用点语法访问任何键的值:

console.log(paolo.city);
复制代码

现在假设键是动态的,我们可以把它放在一个变量中,然后在括号中引用它

const paolo = {
  name: "Paolo",
  city: "Siena",
  age: 44
};

const key = "city";

console.log(paolo[key]);
复制代码

现在咱们添加另一个对象,将它们都放到一个数组中,并使用filter方法对数组进行筛选,就像我们在 filterByTerm.js 中所做的那样。但这一次是动态传递的,因此可以过滤任何对象

const paolo = {
  name: "Paolo",
  city: "Siena",
  age: 44
};

const tom = {
  name: "Tom",
  city: "Munich",
  age: 33
};

function filterPerson(arr, term, key) {
  return arr.filter(function(person) {
    return person[key].match(term);
  });
}

filterPerson([paolo, tom], "Siena", "city");
复制代码

这是比较重要的一行行:

return person[key].match(term);
复制代码

能行吗 是的,因为 JS 不在乎 paolotom 是否可通过动态 [key] 进行“索引化”。 那在 TS 又是怎么样的呢?

在下一部分中,我们将使用动态键使 filterByTerm 更加灵活。

接口可以有索引

让我们回到 filterByTerm.tsfilterByTerm 函数

function filterByTerm(input: Array<ILink>, searchTerm: string) {
  if (!searchTerm) throw Error("searchTerm 不能为空");
  if (!input.length) throw Error("input 不能为空");
  const regex = new RegExp(searchTerm, "i");
  return input.filter(function(arrayElement) {
    return arrayElement.url.match(regex);
  });
}
复制代码

它看起来不那么灵活,因为对于每个 ILink,咱们都使用硬编码方式将属性 url 与正则表达式相匹配。我们希望使动态属性(也就是键)让代码更灵活:

function filterByTerm(
  input: Array<ILink>,
  searchTerm: string,
  lookupKey: string = "url"
) {
  if (!searchTerm) throw Error("searchTerm 不能为空");
  if (!input.length) throw Error("input 不能为空");
  const regex = new RegExp(searchTerm, "i");
  return input.filter(function(arrayElement) {
    return arrayElement[lookupKey].match(regex);
  });
}
复制代码

lookupKey 是动态键,这是给它分配了默认参数 “url”。 接着编译代码:

npm run tsc
复制代码

当然会报错:

error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'ILink'.
  No index signature with a parameter of type 'string' was found on type 'ILink'.
复制代码

出错行:

return arrayElement[lookupKey].match(regex);
复制代码

元素隐式具有 "any" 类型,因为类型 “ILink” 没有索引签名,需要你添加一个索引到对象的接口,这很容易解决。

转到接口 ILink 并添加索引:

interface ILink {
  description?: string;
  id?: number;
  url: string;
  [index: string] : string
}
复制代码

语法有点奇怪,但类似于对象上的动态键访问。这意味着我们可以通过类型为 string 的索引访问该对象的任何键,该索引反过来又返回另一个字符串。

不过,这样写会引发其它错误:

error TS2411: Property 'description' of type 'string | undefined' is not assignable to string index type 'string'.
error TS2411: Property 'id' of type 'number | undefined' is not assignable to string index type 'string'.
复制代码

这是因为接口上的一些属性是可选的,可能是 undefined,而且返回类型不总是string(例如,id 是一个 number)。

interface ILink {
  description?: string;
  id?: number;
  url: string;
  [index: string]: string | number | undefined;
}
复制代码

这一行:

[index: string]: string | number | undefined;
复制代码

表示该索引是一个字符串,可以返回另一个字符串、数字或 undefined。尝试再次编译,这里有另一个错误

error TS2339: Property 'match' does not exist on type 'string | number'.
return arrayElement[lookupKey].match(regex);
复制代码

报的没毛病。match 方法只存在字符串中 ,而且我们的索引有可能返回一个 number。为了修正这个错误,我们可以使用 any 类型:

interface ILink {
  description?: string;
  id?: number;
  url: string;
  [index: string]: any;
}
复制代码

再次编译通过。

函数的返回类型

到目前为止有很多新东西。现在来看看 TypeScript 的另一个有用的特性**:函数的返回类型**。

回到 filterByTerm 函数:

function filterByTerm(
  input: Array<ILink>,
  searchTerm: string,
  lookupKey: string = "url"
) {
  if (!searchTerm) throw Error("searchTerm 不能为空");
  if (!input.length) throw Error("input 不能为空");
  const regex = new RegExp(searchTerm, "i");
  return input.filter(function(arrayElement) {
    return arrayElement[lookupKey].match(regex);
  });
}
复制代码

如果按原样调用,传递前面看到的 ILink 数组和搜索词string3,则如预期的那样返回一个对象数组:

filterByTerm(arrOfLinks, "string3"); 

// EXPECTED OUTPUT:
// [ { url: 'string3' } ]
复制代码

但现在考虑一个更改的变体:

function filterByTerm(
  input: Array<ILink>,
  searchTerm: string,
  lookupKey: string = "url"
) {
  if (!searchTerm) throw Error("searchTerm cannot be empty");
  if (!input.length) throw Error("input cannot be empty");
  const regex = new RegExp(searchTerm, "i");
  return input
    .filter(function(arrayElement) {
      return arrayElement[lookupKey].match(regex);
    })
    .toString();
}
复制代码

如果现在调用,使用相同的 ILink 数组和搜索词 string3,它将返回 [object object]

filterByTerm(arrOfLinks, "string3");

// WRONG OUTPUT:
// [object Object]
复制代码

该函数没有按照预期工作,如果对 JS 隐式类型转换不清楚就很难发现问题。幸运的是,TypeScript 可以捕获这些错误,就像你在编辑器中写的那样。

修正如下:

function filterByTerm(/* 省略 */): Array<ILink> {
 /* 省略 */
}
复制代码

它是如何工作的? 通过在函数体之前添加类型注释,告诉 TypeScript 期望另一个数组作为返回值。现在这个bug 很容易被发现。

interface ILink {
  description?: string;
  id?: number;
  url: string;
  [index: string]: any;
}

function filterByTerm(
  input: Array<ILink>,
  searchTerm: string,
  lookupKey: string = "url"
): Array<ILink> {
  if (!searchTerm) throw Error("searchTerm cannot be empty");
  if (!input.length) throw Error("input cannot be empty");
  const regex = new RegExp(searchTerm, "i");
  return input
    .filter(function(arrayElement) {
      return arrayElement[lookupKey].match(regex);
    })
    .toString();
}

const obj1: ILink = { url: "string1" };
const obj2: ILink = { url: "string2" };
const obj3: ILink = { url: "string3" };

const arrOfLinks: Array<ILink> = [obj1, obj2, obj3];

filterByTerm(arrOfLinks, "string3");
复制代码

现在编译并检查错误:

error TS2322: Type 'string' is not assignable to type 'ILink[]'.
复制代码

咱们希望返回值的是 ILink 数组,而不是字符串。要修复此错误,从末尾删除 .tostring() 并重新编译代码就行了。

类型别名 vs 接口

到目前为止,我们已经将接口视为描述对象和自定义类型的工具。但是通过其他人的代码,你可能也注意到了关键字的 type

显然,interface 和 type 在 TypeScript 中可以互换使用,但是它们在许多方面有所不同,这就是TypeScript 给初学者的困惑。

请记住: TypeScript 中的接口描述是某个东西的结构,大多数情况下是一个复杂的对象。

另一方面,type 也可以用来描述自定义的结构,但它只是一个别名,或者换句话说,是自定义类型的标签。例如,设想一个有两个字段的接口,其中一个是布尔型、数字型和字符串型的联合类型

interface IExample {
  authenticated: boolean | number | string;
  name: string;
}
复制代码

例如,使用 type 别名 可以提取自定义联合类型,并创建名为 Authenticated 的标签

type Authenticated = boolean | number | string;

interface IExample {
  authenticated: Authenticated;
  name: string;
}
复制代码

通过这种方式,咱可以隔离所做的更改,就不必在整个代码库中复制/粘贴 联合类型

如果要将 type 应用上面示例(filterByTerm),创建一个名为 ILinks 的新标签,并将 Array 分配给它。 这样,就可以引用前者:

// the new label
type ILinks = Array<ILink>;
// the new label

function filterByTerm(
  input: ILinks,
  searchTerm: string,
  lookupKey: string = "url"
): ILinks {
  if (!searchTerm) throw Error("searchTerm 不能为空");
  if (!input.length) throw Error("input 不能为空");
  const regex = new RegExp(searchTerm, "i");
  return input.filter(function(arrayElement) {
    return arrayElement[lookupKey].match(regex);
  });
}

const obj1: ILink = { url: "string1" };
const obj2: ILink = { url: "string2" };
const obj3: ILink = { url: "string3" };

const arrOfLinks: ILinks = [obj1, obj2, obj3];

filterByTerm(arrOfLinks, "string3");
复制代码

当然,这不是 type 用法最好事例。那么在 interfacetype 之间使用哪个呢? 我更喜欢复杂对象的接口。TypeScript 文档 也建议了。

一个软件的理想状态是可以扩展,因此,如果可能,应始终在类型别名上使用接口。

更多关于接口和对象的知识点

函数是 JS 中的一等公民,而对象是该语言中最重要的实体。

对象大多是键/值对的容器,它们也可以保存函数,这一点也不奇怪。当一个函数位于一个对象内部时,它可以通过关键字 this 访问“宿主”对象:

const tom = {
  name: "前端小智",
  city: "厦门",
  age: 26,
  printDetails: function() {
    console.log(`${this.name} - ${this.city}`);
  }
};
复制代码

到目前为止,咱们已经看到 TypeScript 接口应用于简单对象,用于描述字符串和数字。 但是他们可以做的更多。 举个例, 使用以下代码创建一个名为 interfaces-functions.ts 的新文件:

const tom = {
  name: "前端小智",
  city: "厦门",
  age: 26,
  printDetails: function() {
    console.log(`${this.name} - ${this.city}`);
  }
};
复制代码

这是一个 JS 对象,咱们使用接口 IPerson 给它加上类型:

interface IPerson {
  name: string;
  city: string;
  age: number;
}

const tom: IPerson = {
  name: "前端小智",
  city: "厦门",
  age: 26,
  printDetails: function() {
    console.log(`${this.name} - ${this.city}`);
  }
};
复制代码

编译代码并查看报错信息:

interfaces-functions.ts:11:3 - error TS2322: Type '{ name: string; city: string; age: number; printDetails: () => void; }' is not assignable to type 'IPerson'.
  Object literal may only specify known properties, and 'printDetails' does not exist in type 'IPerson'.
复制代码

IPerson 没有任何名为printDetails的属性,但更重要的是它应该是一个函数。幸运的是,TypeScript 接口也可以描述函数。如下所示:

interface IPerson {
  name: string;
  city: string;
  age: number;
  printDetails(): void;
}
复制代码

在这里,我们添加了类型函数的属性 printDetails,返回 voidvoid 表示不返回任何值。

实际上,打印到控制台的函数不会返回任何内容。 如果要从 printDetails 返回字符串,则可以将返回类型调整为 string

interface IPerson {
  name: string;
  city: string;
  age: number;
  printDetails(): string;
}

const tom: IPerson = {
  name: "前端小智",
  city: "厦门",
  age: 26,
  printDetails: function() {
    return `${this.name} - ${this.city}`;
  }
};
复制代码

如果函数有参数呢? 在接口中,可以为它们添加类型注释

interface IPerson {
  name: string;
  city: string;
  age: number;
  printDetails(): string;
  anotherFunc(a: number, b: number): number;
}
复制代码

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

原文:www.valentinog.com/blog/typesc…

总结

这里无法涵盖每一个 TypeScript 特性。例如,省略了ES2015类及其与接口或更高级类型(如 Partial )的关系。当然后续会持续介绍。

在这个 TypeScript 教程中,讲了:

  • 变量,函数参数和返回值的类型注释
  • 接口
  • 自定义类型
  • 类型别名

TS 帮助咱们减少一些 JS 代码隐藏的错误。需要重复的是,TypeScript 不能替代测试。 蛤它确实是一个有价值的工具,一开始很难掌握,但完全值得投资。

交流

干货系列文章汇总如下,觉得不错点个Star,欢迎 加群 互相学习。

github.com/qq449245884…

因为篇幅的限制,今天的分享只到这里。如果大家想了解更多的内容的话,可以去扫一扫每篇文章最下面的二维码,然后关注咱们的微信公众号,了解更多的资讯和有价值的内容。

clipboard.png

关注下面的标签,发现更多相似文章
评论