Svelte 中的 TypeScript 支持

在上一篇文章中,我们学习了 Svelte 商店,甚至还实现了我们自己的自定义商店,将应用程序的信息持久化到 Web 存储中。我们还研究了如何使用 transition 指令在 Svelte 中对 DOM 元素实现动画。

现在我们将学习如何在 Svelte 应用程序中使用 TypeScript。首先,我们将学习什么是 TypeScript,以及它能给我们带来哪些好处。然后,我们将看到如何配置我们的项目以使用 TypeScript 文件。最后,我们将回顾我们的应用程序,并了解我们需要进行哪些修改才能充分利用 TypeScript 功能。

先决条件

至少,建议您熟悉核心 HTMLCSSJavaScript 语言,并了解 终端/命令行

您需要一个安装了 node 和 npm 的终端来编译和构建您的应用程序。

目标 学习如何在开发 Svelte 应用程序时配置和使用 TypeScript。

请注意,我们的应用程序完全可以正常工作,将其移植到 TypeScript 是完全可选的。对此有不同的意见,在本节中,我们将简要讨论使用 TypeScript 的优缺点。即使您不打算采用它,本文也将对您有所帮助,让您了解 TypeScript 提供的功能,并帮助您做出自己的决定。如果您对 TypeScript 毫无兴趣,可以跳到下一节,我们将讨论部署 Svelte 应用程序的不同选择、更多资源等等。

与我们一起编写代码

Git

使用以下命令克隆 GitHub 仓库(如果您还没有克隆):

bash
git clone https://github.com/opensas/mdn-svelte-tutorial.git

然后,要进入当前的应用程序状态,请运行:

bash
cd mdn-svelte-tutorial/07-typescript-support

或者直接下载该文件夹的内容

bash
npx degit opensas/mdn-svelte-tutorial/07-typescript-support

请记住运行 npm install && npm run dev 以在开发模式下启动您的应用程序。

REPL

TypeScript:JavaScript 的可选静态类型

TypeScript 是 JavaScript 的超集,它提供了可选的静态类型、类、接口和泛型等功能。TypeScript 的目标是通过其类型系统帮助尽早发现错误,并使 JavaScript 开发更有效率。其中一个主要好处是使 IDE 能够提供更丰富的环境,以便在您键入代码时发现常见错误。

最重要的是,JavaScript 代码是有效的 TypeScript 代码;TypeScript 是 JavaScript 的超集。您可以将大多数 .js 文件重命名为 .ts 文件,它们将正常工作。

我们的 TypeScript 代码将能够在 JavaScript 可运行的任何地方运行。这是如何做到的呢?TypeScript 将我们的代码“转译”为原生 JavaScript。这意味着它会解析 TypeScript 代码,并为浏览器运行生成等效的原生 JavaScript 代码。

注意:如果您好奇 TypeScript 如何将我们的代码转译为 JavaScript,可以查看 TypeScript Playground

长期以来,一流的 TypeScript 支持一直是 Svelte 最受欢迎的功能。感谢 Svelte 团队以及众多贡献者的辛勤工作,我们现在有了可以投入测试的 官方解决方案。在本节中,我们将向您展示如何设置一个支持 TypeScript 的 Svelte 项目,以便您可以试用。

为什么要使用 TypeScript?

TypeScript 的主要优势是

  • 尽早发现错误:编译器在编译时检查类型,并提供错误报告。
  • 可读性:静态类型使代码结构更清晰,使其具有自文档性,更易于阅读。
  • 丰富的 IDE 支持:类型信息允许代码编辑器和 IDE 提供代码导航、自动完成和更智能的提示等功能。
  • 更安全的重构:类型允许 IDE 更多地了解您的代码,并在您重构代码库的大部分代码时提供帮助。
  • 类型推断:即使不声明变量类型,也能让您利用许多 TypeScript 功能。
  • 可以使用新的和未来的 JavaScript 功能:TypeScript 将许多最新的 JavaScript 功能转译为普通的旧式 JavaScript,允许您即使在不支持这些功能的浏览器上也能使用它们。

TypeScript 也有一些缺点

  • 不是真正的静态类型:类型仅在编译时检查,并且从生成的代码中删除。
  • 陡峭的学习曲线:虽然 TypeScript 是 JavaScript 的超集,而不是全新的语言,但它有一个相当大的学习曲线,特别是如果您没有使用 Java 或 C# 等静态语言的经验。
  • 更多代码:您需要编写和维护更多代码。
  • 不能替代自动测试:虽然类型可能帮助您捕获一些错误,但 TypeScript 并不是全面的自动化测试套件的真正替代品。
  • 样板代码:使用类型、类、接口和泛型可能会导致过度设计的代码库。

似乎有广泛的共识,即 TypeScript 特别适合大型项目,多个开发人员在同一个代码库上进行开发。事实上,它正被许多大型项目使用,如 Angular 2、Vue 3、Ionic、Visual Studio Code、Jest,甚至 Svelte 编译器。然而,一些开发人员即使在像我们正在开发的这种小型项目中也更喜欢使用它。

最终,由您决定。在接下来的部分中,我们希望为您提供更多证据,帮助您做出决定。

从头开始创建 Svelte TypeScript 项目

您可以使用 标准模板 启动一个新的 Svelte TypeScript 项目。您只需运行以下终端命令(在您存储 Svelte 测试项目的某个位置运行这些命令,它会创建一个新目录):

bash
npx degit sveltejs/template svelte-typescript-app

cd svelte-typescript-app

node scripts/setupTypeScript.js

这将创建一个包含 TypeScript 支持的入门项目,您可以根据需要对其进行修改。

然后,您需要告诉 npm 下载依赖项并在开发模式下启动项目,就像我们通常做的那样:

bash
npm install

npm run dev

向现有 Svelte 项目添加 TypeScript 支持

要将 TypeScript 支持添加到现有的 Svelte 项目,您可以 按照这些说明操作。或者,您可以将 setupTypeScript.js 文件下载到项目根文件夹中的 scripts 文件夹中,然后运行 node scripts/setupTypeScript.js

您甚至可以使用 degit 下载该脚本。这就是我们将用来开始将我们的应用程序移植到 TypeScript 的方法。

注意:请记住,您可以运行 npx degit opensas/mdn-svelte-tutorial/07-typescript-support svelte-todo-typescript 获取完整的 JavaScript 版待办事项列表应用程序,然后再开始将其移植到 TypeScript。

转到项目的根目录并输入以下命令:

bash
npx degit sveltejs/template/scripts scripts       # download script file to a scripts folder

node scripts/setupTypeScript.js                   # run it
# Converted to TypeScript.

您需要重新运行依赖项管理器才能开始。

bash
npm install                                       # download new dependencies

npm run dev                                       # start the app in development mode

这些说明适用于您想要转换为 TypeScript 的任何 Svelte 项目。请注意,Svelte 社区一直在不断改进 Svelte TypeScript 支持,因此您应该定期运行 npm update 以利用最新的更改。

注意:如果您在 Svelte 应用程序中使用 TypeScript 时遇到任何问题,请查看此 关于 TypeScript 支持的故障排除/常见问题解答部分

正如我们之前所说,TypeScript 是 JavaScript 的超集,因此您的应用程序无需修改即可运行。目前,您将运行一个带有 TypeScript 支持的常规 JavaScript 应用程序,而不会利用 TypeScript 提供的任何功能。您现在可以开始逐步添加类型。

配置好 TypeScript 后,您只需在脚本部分的开头添加 <script lang='ts'> 即可从 Svelte 组件中使用它。要从常规 JavaScript 文件中使用它,只需将文件扩展名从 .js 更改为 .ts。您还需要更新所有相应的导入语句,从所有 import 语句中删除 .ts 文件扩展名。

注意:如果您在 import 语句中使用 .ts 文件扩展名,TypeScript 将抛出错误,因此,如果您有一个文件 ./foo.ts,则必须将其导入为 "./foo"。有关更多信息,请参阅 TypeScript 手册的 捆绑程序、TypeScript 运行时和 Node.js 加载程序的模块解析 部分。

注意:在组件标记部分使用 TypeScript 目前尚不支持。您需要在标记中使用 JavaScript,在 <script lang='ts'> 部分使用 TypeScript。

TypeScript 改善开发体验

TypeScript 为代码编辑器和 IDE 提供了大量信息,使它们能够提供更友好的开发体验。

我们将使用 Visual Studio Code 进行快速测试,看看如何在编写组件时获得自动完成提示和类型检查。

注意:如果您不想使用 VS Code,我们稍后还会提供从终端使用 TypeScript 错误检查的说明。

正在进行一项工作,以便在多个代码编辑器中支持 Svelte 项目中的 TypeScript;目前最完整的支持是在 Svelte for VS Code 扩展 中,该扩展由 Svelte 团队开发和维护。此扩展提供类型检查、检查、重构、智能感知、悬停信息、自动完成和其他功能。这种开发辅助功能是您在项目中开始使用 TypeScript 的另一个很好的理由。

注意:确保您使用的是 Svelte for VS Code,而不是旧的 “Svelte” (由 James Birtles 开发),该扩展已停止维护。如果您安装了它,则应将其卸载,并改用官方的 Svelte 扩展。

假设您已在 VS Code 应用程序中,从项目文件夹的根目录中,键入 code .(尾部的点告诉 VS Code 打开当前文件夹)以打开代码编辑器。VS Code 会告诉您有一些推荐的扩展需要安装。

Dialog box saying this workspace has extension recommendations, with options to install or show a list

单击安装全部将安装 Svelte for VS Code。

Svelte for VS Code extension information

我们还可以看到,setupTypeScript.js 文件对我们的项目进行了一些更改。main.js 文件已重命名为 main.ts,这意味着 VS Code 可以提供关于 Svelte 组件的悬停信息

VS Code screenshot showing that when hovering on a component, it gives you hints

我们还可以免费获得类型检查。如果我们在 App 构造函数的 options 参数中传递了一个未知属性(例如,像 traget 而不是 target 这样的拼写错误),TypeScript 会报错

Type checking in VS Code - App object has been given an unknown property traget

App.svelte 组件中,setupTypeScript.js 脚本已将 lang="ts" 属性添加到 <script> 标签中。此外,由于类型推断,在许多情况下我们甚至不需要指定类型就能获得代码提示。例如,如果您开始向 Alert 组件调用中添加 ms 属性,TypeScript 会从默认值推断出 ms 属性应该是一个数字。

VS Code type inference and code hinting - ms variable should be a number

如果您传递的不是数字,它会报错。

Type checking in VS Code - the ms variable has been given a non-numeric value

应用程序模板配置了 check 脚本,它会对您的代码运行 svelte-check。此包允许您从命令行检测代码编辑器通常显示的错误和警告,这使其在持续集成 (CI) 管道中运行非常有用。只需运行 npm run check 即可检查未使用的 CSS,并返回 A11y 提示和 TypeScript 编译错误。

在这种情况下,如果您运行 npm run check(在 VS Code 控制台或终端中),您将收到以下错误

Check command being run inside VS Code showing type error, ms variable should be assigned a number

更好的是,如果您从 VS Code 集成终端运行它(您可以使用 Ctrl + ` 键盘快捷键打开它),Cmd/Ctrl 点击文件名将带您到包含错误的行。

您还可以使用 npm run check -- --watch 以监视模式运行 check 脚本。在这种情况下,只要您更改任何文件,脚本就会执行。如果您在常规终端中运行它,请在一个单独的终端窗口中后台运行它,以便它可以继续报告错误,但不会干扰其他终端使用。

创建自定义类型

TypeScript 支持结构类型。结构类型是一种根据类型成员来关联类型的方式,即使您没有显式定义类型。

我们将定义一个 TodoType 类型,看看 TypeScript 如何强制执行传递给期望 TodoType 的组件的任何内容都将与其结构兼容。

  1. src 文件夹中创建一个 types 文件夹。
  2. 在其中添加一个 todo.type.ts 文件。
  3. todo.type.ts 提供以下内容
    ts
    export type TodoType = {
      id: number;
      name: string;
      completed: boolean;
    };
    

    注意: Svelte 模板使用 svelte-preprocess 4.0.0 来支持 TypeScript。从该版本开始,您必须使用 export/import 类型语法导入类型和接口。查看 故障排除指南的这一部分 以获取更多信息。

  4. 现在我们将从我们的 Todo.svelte 组件中使用 TodoType。首先将 lang="ts" 添加到我们的 <script> 标签中。
  5. 让我们import 该类型并使用它来声明 todo 属性。用以下内容替换 export let todo
    ts
    import type { TodoType } from "../types/todo.type";
    
    export let todo: TodoType;
    
    请注意,.ts 文件扩展名在 import 语句中是不允许的,并且已被省略。
  6. 现在,从 Todos.svelte 中,我们将使用字面量对象作为其参数在调用 MoreActions 组件之前实例化一个 Todo 组件,如下所示
    svelte
    <hr />
    
    <Todo todo={ { name: 'a new task with no id!', completed: false } } />
    
    <!-- MoreActions -->
    <MoreActions {todos}
    
  7. lang='ts' 添加到 Todos.svelte 组件的 <script> 标签中,以便它知道使用我们指定的类型检查。我们将收到以下错误:VS Code 中的类型错误,Todo Type 对象需要 id 属性。

到目前为止,您应该对我们在构建 Svelte 项目时从 TypeScript 获得的帮助类型有所了解。

现在,我们将撤消这些更改,以便开始将我们的应用程序移植到 TypeScript,因此我们不会被所有检查警告困扰。

  1. Todos.svelte 文件中删除有缺陷的待办事项和 lang='ts' 属性。
  2. 还从 Todo.svelte 中删除 TodoType 的导入和 lang='ts'

我们稍后会妥善处理它们。

将待办事项应用程序移植到 TypeScript

现在我们准备开始将我们的待办事项列表应用程序移植以利用 TypeScript 提供给我们的所有功能。

让我们从在项目根目录中以监视模式运行检查脚本开始

bash
npm run check -- --watch

这应该输出类似于以下内容的内容

bash
svelte-check "--watch"

Loading svelte-check in workspace: ./svelte-todo-typescript
Getting Svelte diagnostics...
====================================
svelte-check found no errors and no warnings

请注意,如果您使用的是像 VS Code 这样的支持代码编辑器,开始移植 Svelte 组件的一种简单方法就是在组件顶部添加 <script lang='ts'>,然后查找三个点的提示

VS Code screenshot showing that when you add type="ts" to a component, it gives you three dot alert hints

Alert.svelte

让我们从我们的 Alert.svelte 组件开始。

  1. lang="ts" 添加到您的 Alert.svelte 组件的 <script> 标签中。您将在 check 脚本的输出中看到一些警告
    bash
    npm run check -- --watch
    
    > svelte-check "--watch"
    
    ./svelte-todo-typescript
    Getting Svelte diagnostics...
    ====================================
    
    ./svelte-todo-typescript/src/components/Alert.svelte:8:7
    Warn: Variable 'visible' implicitly has an 'any' type, but a better type may be inferred from usage. (ts)
      let visible
    
    ./svelte-todo-typescript/src/components/Alert.svelte:9:7
    Warn: Variable 'timeout' implicitly has an 'any' type, but a better type may be inferred from usage. (ts)
      let timeout
    
    ./svelte-todo-typescript/src/components/Alert.svelte:11:28
    Warn: Parameter 'message' implicitly has an 'any' type, but a better type may be inferred from usage. (ts)
    Change = (message, ms) => {
    
    ./svelte-todo-typescript/src/components/Alert.svelte:11:37
    Warn: Parameter 'ms' implicitly has an 'any' type, but a better type may be inferred from usage. (ts)
    (message, ms) => {
    
  2. 您可以通过指定相应的类型来修复它们,如下所示
    ts
    export let ms = 3000
    
      let visible: boolean
      let timeout: number
    
      const onMessageChange = (message: string, ms: number) => {
        clearTimeout(timeout)
        if (!message) {               // hide Alert if message is empty
    

    注意: 无需使用 export let ms:number = 3000 指定 ms 类型,因为 TypeScript 已经从其默认值推断出它。

MoreActions.svelte

现在我们对 MoreActions.svelte 组件做同样的事情。

  1. 像以前一样添加 lang='ts' 属性。TypeScript 会警告我们关于 todos 属性和在调用 todos.filter((t) =>...) 中的 t 变量。
    Warn: Variable 'todos' implicitly has an 'any' type, but a better type may be inferred from usage. (ts)
      export let todos
    
    Warn: Parameter 't' implicitly has an 'any' type, but a better type may be inferred from usage. (ts)
      $: completedTodos = todos.filter((t) => t.completed).length
    
  2. 我们将使用我们已经定义的 TodoType 来告诉 TypeScript todos 是一个 TodoType 数组。用以下内容替换 export let todos
    ts
    import type { TodoType } from "../types/todo.type";
    
    export let todos: TodoType[];
    

请注意,现在 TypeScript 可以推断出 todos.filter((t) => t.completed) 中的 t 变量是 TodoType 类型。然而,如果我们认为这使我们的代码更易于阅读,我们可以像这样指定它

ts
$: completedTodos = todos.filter((t: TodoType) => t.completed).length;

大多数情况下,TypeScript 将能够正确推断出响应式变量类型,但有时您在处理响应式赋值时可能会遇到“隐式具有 'any' 类型”错误。在这些情况下,您可以像这样在不同的语句中声明类型化变量

ts
let completedTodos: number;
$: completedTodos = todos.filter((t: TodoType) => t.completed).length;

您不能在响应式赋值本身中指定类型。语句 $: completedTodos: number = todos.filter[...] 是无效的。有关更多信息,请阅读 如何为响应式赋值指定类型?/ 我遇到了“隐式具有 'any' 类型”错误

FilterButton.svelte

现在我们将处理 FilterButton 组件。

  1. 像往常一样将 lang='ts' 属性添加到 <script> 标签中。您会注意到没有警告 - TypeScript 从默认值推断出过滤器变量的类型。但我们知道过滤器只有三个有效值:all、active 和 completed。因此,我们可以通过创建一个枚举 Filter 来让 TypeScript 知道它们。
  2. types 文件夹中创建一个 filter.enum.ts 文件。
  3. 为其提供以下内容
    ts
    export enum Filter {
      ALL = "all",
      ACTIVE = "active",
      COMPLETED = "completed",
    }
    
  4. 现在我们将从 FilterButton 组件中使用它。用以下内容替换 FilterButton.svelte 文件的内容
    svelte
    <!-- components/FilterButton.svelte -->
    <script lang="ts">
      import { Filter } from "../types/filter.enum";
    
      export let filter: Filter = Filter.ALL;
    </script>
    
    <div class="filters btn-group stack-exception">
      <button class="btn toggle-btn" class:btn__primary={filter === Filter.ALL} aria-pressed={filter === Filter.ALL} on:click={()=> filter = Filter.ALL} >
        <span class="visually-hidden">Show</span>
        <span>All</span>
        <span class="visually-hidden">tasks</span>
      </button>
      <button class="btn toggle-btn" class:btn__primary={filter === Filter.ACTIVE} aria-pressed={filter === Filter.ACTIVE} on:click={()=> filter = Filter.ACTIVE} >
        <span class="visually-hidden">Show</span>
        <span>Active</span>
        <span class="visually-hidden">tasks</span>
      </button>
      <button class="btn toggle-btn" class:btn__primary={filter === Filter.COMPLETED} aria-pressed={filter === Filter.COMPLETED} on:click={()=> filter = Filter.COMPLETED} >
        <span class="visually-hidden">Show</span>
        <span>Completed</span>
        <span class="visually-hidden">tasks</span>
      </button>
    </div>
    

在这里,我们只是导入 Filter 枚举,并使用它来代替我们以前使用的字符串值。

Todos.svelte

我们还将在 Todos.svelte 组件中使用 Filter 枚举。

  1. 首先,像以前一样将 lang='ts' 属性添加到其中。
  2. 接下来,导入 Filter 枚举。在您现有的导入语句下方添加以下 import 语句
    js
    import { Filter } from "../types/filter.enum";
    
  3. 现在,无论何时引用当前过滤器,我们都将使用它。用以下内容替换您与过滤器相关的两个块
    ts
    let filter: Filter = Filter.ALL;
    const filterTodos = (filter: Filter, todos) =>
      filter === Filter.ACTIVE
        ? todos.filter((t) => !t.completed)
        : filter === Filter.COMPLETED
          ? todos.filter((t) => t.completed)
          : todos;
    
    $: {
      if (filter === Filter.ALL) {
        $alert = "Browsing all todos";
      } else if (filter === Filter.ACTIVE) {
        $alert = "Browsing active todos";
      } else if (filter === Filter.COMPLETED) {
        $alert = "Browsing completed todos";
      }
    }
    
  4. check 仍然会从 Todos.svelte 中给我们一些警告。让我们修复它们。首先导入 TodoType 并告诉 TypeScript 我们的 todos 变量是一个 TodoType 数组。用以下两行替换 export let todos = []
    ts
    import type { TodoType } from "../types/todo.type";
    
    export let todos: TodoType[] = [];
    
  5. 接下来,我们将指定所有缺少的类型。我们用来以编程方式访问 TodosStatus 组件公开的方法的变量 todosStatusTodosStatus 类型。每个 todo 将是 TodoType 类型。更新您的 <script> 部分,使其看起来像这样
    ts
    import FilterButton from "./FilterButton.svelte";
    import Todo from "./Todo.svelte";
    import MoreActions from "./MoreActions.svelte";
    import NewTodo from "./NewTodo.svelte";
    import TodosStatus from "./TodosStatus.svelte";
    import { alert } from "../stores";
    
    import { Filter } from "../types/filter.enum";
    
    import type { TodoType } from "../types/todo.type";
    
    export let todos: TodoType[] = [];
    
    let todosStatus: TodosStatus; // reference to TodosStatus instance
    
    $: newTodoId =
      todos.length > 0 ? Math.max(...todos.map((t) => t.id)) + 1 : 1;
    
    function addTodo(name: string) {
      todos = [...todos, { id: newTodoId, name, completed: false }];
      $alert = `Todo '${name}' has been added`;
    }
    
    function removeTodo(todo: TodoType) {
      todos = todos.filter((t) => t.id !== todo.id);
      todosStatus.focus(); // give focus to status heading
      $alert = `Todo '${todo.name}' has been deleted`;
    }
    
    function updateTodo(todo: TodoType) {
      const i = todos.findIndex((t) => t.id === todo.id);
      if (todos[i].name !== todo.name)
        $alert = `todo '${todos[i].name}' has been renamed to '${todo.name}'`;
      if (todos[i].completed !== todo.completed)
        $alert = `todo '${todos[i].name}' marked as ${
          todo.completed ? "completed" : "active"
        }`;
      todos[i] = { ...todos[i], ...todo };
    }
    
    let filter: Filter = Filter.ALL;
    const filterTodos = (filter: Filter, todos: TodoType[]) =>
      filter === Filter.ACTIVE
        ? todos.filter((t) => !t.completed)
        : filter === Filter.COMPLETED
          ? todos.filter((t) => t.completed)
          : todos;
    
    $: {
      if (filter === Filter.ALL) {
        $alert = "Browsing all todos";
      } else if (filter === Filter.ACTIVE) {
        $alert = "Browsing active todos";
      } else if (filter === Filter.COMPLETED) {
        $alert = "Browsing completed todos";
      }
    }
    
    const checkAllTodos = (completed: boolean) => {
      todos = todos.map((t) => ({ ...t, completed }));
      $alert = `${completed ? "Checked" : "Unchecked"} ${todos.length} todos`;
    };
    const removeCompletedTodos = () => {
      $alert = `Removed ${todos.filter((t) => t.completed).length} todos`;
      todos = todos.filter((t) => !t.completed);
    };
    

TodosStatus.svelte

我们遇到以下与将 todos 传递给 TodosStatus.svelte(和 Todo.svelte)组件相关的错误

./src/components/Todos.svelte:70:39
Error: Type 'TodoType[]' is not assignable to type 'undefined'. (ts)
  <TodosStatus bind:this={todosStatus} {todos} />

./src/components/Todos.svelte:76:12
Error: Type 'TodoType' is not assignable to type 'undefined'. (ts)
     <Todo {todo}

这是因为 TodosStatus 组件中的 todos 属性没有默认值,因此 TypeScript 推断出它是 undefined 类型,与 TodoType 数组不兼容。我们的 Todo 组件也是如此。

让我们修复它。

  1. 打开 TodosStatus.svelte 文件并添加 lang='ts' 属性。
  2. 然后导入 TodoType 并将 todos 属性声明为 TodoType 数组。用以下内容替换 <script> 部分的第一行
    ts
    import type { TodoType } from "../types/todo.type";
    
    export let todos: TodoType[];
    
  3. 我们还将指定 headingEl(我们用来绑定到标题标签的元素)为 HTMLElement。用以下内容更新 let headingEl
    ts
    let headingEl: HTMLElement;
    
  4. 最后,您会注意到报告以下错误,与我们设置 tabindex 属性的位置相关。这是因为 TypeScript 正在对 <h2> 元素进行类型检查,并期望 tabindexnumber 类型。 VS Code 中的 Tabindex 提示,tabindex 期望类型为数字,而不是字符串 要修复它,请将 tabindex="-1" 替换为 tabindex={-1},如下所示
    svelte
    <h2 id="list-heading" bind:this={headingEl} tabindex={-1}>
      {completedTodos} out of {totalTodos} items completed
    </h2>
    
    这样 TypeScript 就可以防止我们错误地将其分配给字符串变量。

NewTodo.svelte

接下来我们将处理 NewTodo.svelte

  1. 像往常一样,添加 lang='ts' 属性。
  2. 警告将表明我们必须为 nameEl 变量指定一个类型。将其类型设置为 HTMLElement,如下所示
    ts
    let nameEl: HTMLElement; // reference to the name input DOM node
    
  3. 最后,对于此文件,我们需要为我们的 autofocus 变量指定正确的类型。像这样更新其定义
    ts
    export let autofocus: boolean = false;
    

Todo.svelte

现在,npm run check 发出的唯一警告是由调用 Todo.svelte 组件触发的。让我们修复它们。

  1. 打开 Todo.svelte 文件,并添加 lang='ts' 属性。
  2. 让我们导入 TodoType 并设置 todo 属性的类型。用以下内容替换 export let todo
    ts
    import type { TodoType } from "../types/todo.type";
    
    export let todo: TodoType;
    
  3. 我们收到的第一个警告是 TypeScript 告诉我们定义 update() 函数的 updatedTodo 变量的类型。这可能有点棘手,因为 updatedTodo 只包含已更新的 todo 的属性。这意味着它不是一个完整的 todo - 它只包含 todo 属性的一个子集。对于这些情况,TypeScript 提供了几种 实用程序类型 来简化这些常见转换的应用。我们现在需要的是 Partial<T> 实用程序,它允许我们表示给定类型的全部子集。部分实用程序根据类型 T 返回一个新类型,其中 T 的每个属性都是可选的。我们将在 update() 函数中使用它 - 像这样更新您的函数
    ts
    function update(updatedTodo: Partial<TodoType>) {
      todo = { ...todo, ...updatedTodo }; // applies modifications to todo
      dispatch("update", todo); // emit update event
    }
    
    通过此操作,我们告诉 TypeScript updatedTodo 变量将保存 TodoType 属性的一个子集。
  4. 现在 svelte-check 告诉我们必须定义操作函数参数的类型
    bash
    ./07-next-steps/src/components/Todo.svelte:45:24
    Warn: Parameter 'node' implicitly has an 'any' type, but a better type may be inferred from usage. (ts)
      const focusOnInit = (node) => node && typeof node.focus === 'function' && node.focus()
    
    ./07-next-steps/src/components/Todo.svelte:47:28
    Warn: Parameter 'node' implicitly has an 'any' type, but a better type may be inferred from usage. (ts)
      const focusEditButton = (node) => editButtonPressed && node.focus()
    
    我们只需要将 node 变量定义为 HTMLElement 类型。在上面指示的两行中,将 node 的第一个实例替换为 node: HTMLElement

actions.js

接下来我们将处理 actions.js 文件。

  1. 将其重命名为 actions.ts 并添加节点参数的类型。它最终应该看起来像这样
    ts
    // actions.ts
    export function selectOnFocus(node: HTMLInputElement) {
      if (node && typeof node.select === "function") {
        // make sure node is defined and has a select() method
        const onFocus = () => node.select(); // event handler
        node.addEventListener("focus", onFocus); // when node gets focus call onFocus()
        return {
          destroy: () => node.removeEventListener("focus", onFocus), // this will be executed when the node is removed from the DOM
        };
      }
    }
    
  2. 现在更新 Todo.svelteNewTodo.svelte,在其中导入 actions 文件。请记住,TypeScript 中的导入不包含文件扩展名。在每种情况下,最终应如下所示
    js
    import { selectOnFocus } from "../actions";
    

将存储迁移到 TypeScript

现在,我们需要将 stores.jslocalStore.js 文件迁移到 TypeScript。

提示:脚本 npm run check(使用 svelte-check 工具)只会检查我们应用程序的 .svelte 文件。如果您还想检查 .ts 文件,可以运行 npm run check && npx tsc --noemit,它会告诉 TypeScript 编译器检查错误,但不生成 .js 输出文件。您甚至可以在 package.json 文件中添加一个运行该命令的脚本。

我们将从 stores.js 开始。

  1. 将文件重命名为 stores.ts
  2. initialTodos 数组的类型设置为 TodoType[]。内容最终将如下所示
    ts
    // stores.ts
    import { writable } from "svelte/store";
    import { localStore } from "./localStore.js";
    import type { TodoType } from "./types/todo.type";
    
    export const alert = writable("Welcome to the To-Do list app!");
    
    const initialTodos: TodoType[] = [
      { id: 1, name: "Visit MDN web docs", completed: true },
      { id: 2, name: "Complete the Svelte Tutorial", completed: false },
    ];
    
    export const todos = localStore("mdn-svelte-todo", initialTodos);
    
  3. 请记住更新 App.svelteAlert.svelteTodos.svelte 中的 import 语句。只需删除 .js 扩展名,如下所示
    js
    import { todos } from "../stores";
    

现在继续 localStore.js

更新 stores.ts 中的 import 语句,如下所示

js
import { localStore } from "./localStore";
  1. 首先将文件重命名为 localStore.ts
  2. TypeScript 告诉我们要指定 keyinitialvalue 变量的类型。第一个很简单:我们本地网页存储的键应该是一个字符串。但是,initialvalue 应该可以转换为使用 JSON.stringify 方法的有效 JSON 字符串的任何对象,这意味着任何 JavaScript 对象,但有一些限制:例如,undefined、函数和符号不是有效的 JSON 值。因此,我们将创建类型 JsonValue 来指定这些条件。在 types 文件夹中创建文件 json.type.ts
  3. 为其提供以下内容
    ts
    export type JsonValue =
      | string
      | number
      | boolean
      | null
      | JsonValue[]
      | { [key: string]: JsonValue };
    
    | 运算符允许我们声明可以存储两种或多种类型值的变量。JsonValue 可以是字符串、数字、布尔值等。在本例中,我们还使用递归类型来指定 JsonValue 可以包含 JsonValue 数组,以及包含类型为 JsonValue 的属性的对象。
  4. 我们将导入 JsonValue 类型并相应地使用它。更新 localStore.ts 文件,如下所示
    ts
    // localStore.ts
    import { writable } from "svelte/store";
    
    import type { JsonValue } from "./types/json.type";
    
    export const localStore = (key: string, initial: JsonValue) => {
      // receives the key of the local storage and an initial value
    
      const toString = (value: JsonValue) => JSON.stringify(value, null, 2); // helper function
      const toObj = JSON.parse; // helper function
    
      if (localStorage.getItem(key) === null) {
        // item not present in local storage
        localStorage.setItem(key, toString(initial)); // initialize local storage with initial value
      }
    
      const saved = toObj(localStorage.getItem(key)); // convert to object
    
      const { subscribe, set, update } = writable(saved); // create the underlying writable store
    
      return {
        subscribe,
        set: (value: JsonValue) => {
          localStorage.setItem(key, toString(value)); // save also to local storage as a string
          return set(value);
        },
        update,
      };
    };
    

现在,如果我们尝试使用无法通过 JSON.stringify() 转换为 JSON 的内容创建 localStore(例如,包含函数作为属性的对象),VS Code/validate 会对此进行抱怨

VS Code showing an error with using our store — it fails when trying to set a local storage value to something incompatible with JSON stringify

最棒的是,它甚至可以使用 $store 自动订阅语法。如果我们尝试使用 $store 语法将无效值保存到我们的 todos 存储中,如下所示

svelte
<!-- App.svelte -->
<script lang="ts">
  import Todos from "./components/Todos.svelte";
  import Alert from "./components/Alert.svelte";

  import { todos } from "./stores";

  // this is invalid, the content cannot be converted to JSON using JSON.stringify
  $todos = { handler: () => {} };
</script>

检查脚本将报告以下错误

bash
> npm run check

Getting Svelte diagnostics...
====================================

./svelte-todo-typescript/src/App.svelte:8:12
Error: Argument of type '{ handler: () => void; }' is not assignable to parameter of type 'JsonValue'.
  Types of property 'handler' are incompatible.
    Type '() => void' is not assignable to type 'JsonValue'.
      Type '() => void' is not assignable to type '{ [key: string]: JsonValue; }'.
        Index signature is missing in type '() => void'. (ts)
 $todos = { handler: () => {} }

这是指定类型如何使我们的代码更健壮并帮助我们在错误进入生产环境之前捕获更多错误的另一个示例。

就是这样。我们已将整个应用程序转换为使用 TypeScript。

使用泛型使我们的存储库更健壮

我们的存储已移植到 TypeScript,但我们还可以做得更好。我们不应该需要存储任何类型的值 - 我们知道警报存储应包含字符串消息,而待办事项存储应包含 TodoType 数组等。我们可以使用 TypeScript 泛型 让 TypeScript 强制执行这一点。让我们了解更多信息。

了解 TypeScript 泛型

泛型允许您创建可重用代码组件,这些组件可用于各种类型,而不是单个类型。它们可以应用于接口、类和函数。泛型类型作为参数传递,使用特殊的语法:它们在尖括号内指定,并且通常用单个大写字母表示。泛型类型允许您捕获用户提供的类型,确保它们可用于以后的处理。

让我们看一个简单的示例,一个简单的 Stack 类,它允许我们像这样 pushpop 元素

ts
export class Stack {
  private elements = [];

  push = (element) => this.elements.push(element);

  pop() {
    if (this.elements.length === 0) throw new Error("The stack is empty!");
    return this.elements.pop();
  }
}

在本例中,elements 是类型为 any 的数组,因此,push()pop() 方法都接收和返回类型为 any 的变量。因此,执行以下操作是完全有效的

js
const anyStack = new Stack();

anyStack.push(1);
anyStack.push("hello");

但如果我们想要一个只对类型 string 起作用的 Stack 该怎么办?我们可以这样做

ts
export class StringStack {
  private elements: string[] = [];

  push = (element: string) => this.elements.push(element);

  pop(): string {
    if (this.elements.length === 0) throw new Error("The stack is empty!");
    return this.elements.pop();
  }
}

那将起作用。但是,如果我们想使用数字,我们必须复制我们的代码并创建一个 NumberStack 类。我们如何处理我们尚不知道的类型堆栈,并且应该由消费者定义?

为了解决所有这些问题,我们可以使用泛型。

这是我们使用泛型重新实现的 Stack

ts
export class Stack<T> {
  private elements: T[] = [];

  push = (element: T): number => this.elements.push(element);

  pop(): T {
    if (this.elements.length === 0) throw new Error("The stack is empty!");
    return this.elements.pop();
  }
}

我们定义了一个泛型类型 T,然后像使用特定类型一样使用它。现在,elements 是类型为 T 的数组,push()pop() 都接收和返回类型为 T 的变量。

以下是使用泛型 Stack 的方法

ts
const numberStack = new Stack<number>();
numberStack.push(1);

现在,TypeScript 知道我们的堆栈只能接受数字,如果我们尝试推送其他内容,它将发出错误

Argument of type hello is not assignable to parameter of type number

TypeScript 也可以通过其用法推断出泛型类型。泛型还支持默认值和约束。

泛型是一个强大的功能,它允许我们的代码从正在使用的特定类型中抽象出来,从而使其更具可重用性和通用性,而不会放弃类型安全。要详细了解它,请查看 TypeScript 泛型简介

将 Svelte 存储与泛型一起使用

Svelte 存储开箱即用地支持泛型。而且,由于泛型类型推断,我们可以利用它,甚至不用修改我们的代码。

如果您打开 Todos.svelte 文件并将 number 类型分配给我们的 $alert 存储,您将收到以下错误

Argument of type 9999 is not assignable to parameter of type string

这是因为当我们在 stores.ts 文件中使用以下内容定义警报存储时

js
export const alert = writable("Welcome to the To-Do list app!");

TypeScript 推断出泛型类型为 string。如果我们想明确地指定它,可以执行以下操作

ts
export const alert = writable<string>("Welcome to the To-Do list app!");

现在,我们将使 localStore 存储支持泛型。请记住,我们定义了 JsonValue 类型以防止使用 JSON.stringify() 无法持久化的值的 localStore 存储。现在,我们希望 localStore 的使用者能够指定要持久化的数据的类型,但他们应该遵守 JsonValue 类型,而不是使用任何类型。我们将使用泛型约束来指定这一点,如下所示

ts
export const localStore = <T extends JsonValue>(key: string, initial: T)

我们定义了一个泛型类型 T,并指定它必须与 JsonValue 类型兼容。然后,我们将适当地使用 T 类型。

我们的 localStore.ts 文件将如下所示 - 现在尝试在您的版本中使用新的代码

ts
// localStore.ts
import { writable } from "svelte/store";

import type { JsonValue } from "./types/json.type";

export const localStore = <T extends JsonValue>(key: string, initial: T) => {
  // receives the key of the local storage and an initial value

  const toString = (value: T) => JSON.stringify(value, null, 2); // helper function
  const toObj = JSON.parse; // helper function

  if (localStorage.getItem(key) === null) {
    // item not present in local storage
    localStorage.setItem(key, toString(initial)); // initialize local storage with initial value
  }

  const saved = toObj(localStorage.getItem(key)); // convert to object

  const { subscribe, set, update } = writable<T>(saved); // create the underlying writable store

  return {
    subscribe,
    set: (value: T) => {
      localStorage.setItem(key, toString(value)); // save also to local storage as a string
      return set(value);
    },
    update,
  };
};

而且,由于泛型类型推断,TypeScript 已经知道我们的 $todos 存储应该包含 TodoType 数组

Todo Type object property complete should be completed

再次说明,如果我们想明确地指定它,可以在 stores.ts 文件中执行以下操作

ts
const initialTodos: TodoType[] = [
  { id: 1, name: "Visit MDN web docs", completed: true },
  { id: 2, name: "Complete the Svelte Tutorial", completed: false },
];

export const todos = localStore<TodoType[]>("mdn-svelte-todo", initialTodos);

对于我们关于 TypeScript 泛型的简短介绍,这就可以了。

迄今为止的代码

Git

要查看本文结尾处的代码状态,请通过以下方式访问您仓库的副本

bash
cd mdn-svelte-tutorial/08-next-steps

或者直接下载该文件夹的内容

bash
npx degit opensas/mdn-svelte-tutorial/08-next-steps

请记住运行 npm install && npm run dev 以在开发模式下启动您的应用程序。

REPL

如前所述,REPL 中尚不支持 TypeScript。

总结

在本文中,我们使用 TypeScript 移植了我们的待办事项列表应用程序。

我们首先了解了 TypeScript 以及它可以带给我们哪些优势。然后,我们看到了如何在具有 TypeScript 支持的情况下创建一个新的 Svelte 项目。我们还看到了如何将现有 Svelte 项目转换为使用 TypeScript - 我们的待办事项列表应用程序。

我们看到了如何使用 Visual Studio CodeSvelte 扩展 来获取类型检查和自动完成功能。我们还使用 svelte-check 工具从命令行检查 TypeScript 问题。

在下一篇文章中,我们将学习如何将应用程序编译并部署到生产环境。我们还将看到在线有哪些资源可用于进一步学习 Svelte。