TypeScript中的动态数据的实例教程

447 阅读6分钟

真的很喜欢静态类型。非常喜欢。当我本来要使用JavaScript时,我现在已经完全接受了TypeScript。

充分利用静态类型,以及它们提供的所有安全性,在处理动态数据时可能有点棘手--比如来自API调用的JSON。这个问题不是TypeScript独有的,但TypeScript确实有一些相当独特的考虑。

让我们考虑一些我们想要解析的JSON。

const rawJson = `{
    "name": "Alice",
    "age": 31
}`
const parsed = JSON.parse(rawJson)  //'parsed' is type 'any'

这也是我们在JavaScript中的做法。事实上,如果你在IDE中把鼠标移到parsed ,你会看到它的类型是 any.这意味着TypeScript会让我们对parsed 做任何事情,而不给我们任何静态类型检查。例如,我们可能会打错字。

console.log(parsed.nam)  //prints 'undefined'

TypeScript不会捕捉到错误;它将愉快地打印出undefined

避免any

为了从TypeScript中获得最大利益,你真的应该尽可能避免使用any1当你的代码中有一些地方通过any 绕过类型系统时,就很难相信你的静态类型。在你真的不知道类型的情况下(比如在解析了一些原始JSON之后),使用 unknown,这是一种类型安全的对应方法,any

例如,我们可以像这样定义一个更安全的解析函数。

const parseJson = (str: string): unknown => JSON.parse(str)

主体只是一个传递,但unknown 返回类型注释使类型更窄。现在,如果我们试图访问我们解析过的JSON中的任何属性,我们会得到一个类型错误。

const parsed = parseJson(rawJson)

console.log(parsed.nam)   //type error: Object is of type 'unknown'.
console.log(parsed.name)  //also a type error - we'll come back to this one :)

超级安全,但还不是很有用。这没关系--这将迫使我们对类型进行明确说明,我们将在下面看到。

类型断言

首先,让我们定义一个符合JSON的类型。

type User = {
    name: string
    age: number
}

有了这个,我们现在可以在解析的值上使用User类型断言来获得我们的静态类型。

const parsed = parseJson(rawJson) as User

console.log(parsed.nam)   //type error: Property 'nam' does not exist on type
console.log(parsed.name)  //works

这有效地告诉TypeScript,"我知道一些你不知道的东西;在这里相信我。"

更进一步

类型断言是简单而有效的,但有一个问题:TypeScript在运行时不做任何验证以确保你的断言是正确的。如果数据的形状出乎意料,或者你声明的类型不正确,你很可能会出现错误,但这些错误可能发生在远离你最初断言类型的地方。这可能会使你很难追踪到确切的问题。

对简单的对象做自己的验证可能是可行的,但这很快就会变得乏味,特别是当你的对象变大或有任何形式的嵌套。

其他语言是如何处理这个问题的?

为了充分理解我们所处的窘境,看看其他静态语言是如何将动态数据转化为类型化对象的,是很有帮助的。

许多语言--如Java、C#和Go--在运行时都有类型信息,可以通过反射访问。这些语言可以使用来自类的类型信息,将JSON反序列化为类型良好的对象。

像Rust这样的语言有宏,可以在构建时为给定的结构自动生成解码器

那些既没有反射,也没有宏的语言,通常有库来手动构建这些解码器。Elm就是一个很好的例子

TypeScript 属于后一种没有反射或宏的语言,所以我们必须走手工路线。

手动解码

我所见过的在TypeScript中编写这些解码器的两个主要库是 io-tsruntypes.

如果你来自函数式编程背景,你可能会喜欢io-ts 。否则,你可能会发现runtypes 更容易接近。

让我们简单看一下如何在runtypes 中构建解码器。

import { Record, String, Number } from 'runtypes'

const UserRuntype = Record({
    name: String,
    age: Number
})

就这样了。这几乎和声明一个TypeScript类型一样简单,它将为我们提供验证数据的方法。

import { Record, String, Number } from 'runtypes'

const UserRuntype = Record({
    name: String,
    age: Number
})

type User = {
    name: string
    age: number
}

const rawJson = `{
    "name": "Alice",
    "age": 31
}`
const user = parseJson(rawJson)

const printUser = (user: User) => {
    console.log(`User ${user.name} is ${user.age} years old`)
}

if (UserRuntype.guard(user))
    printUser(user)

最后使用的guard 方法是一个类型保护,用于安全地检查一个对象是否符合我们的类型。在if 语句中,类型被细化为
{ name: string, age: number } 类型--基本上就是我们上面定义的User 类型。

看到双倍

你可能注意到,我们基本上定义了两次相同的类型。

const UserRuntype = Record({
    name: String,
    age: Number
})

type User = {
    name: string
    age: number
}

既要定义TypeScript类型,又要定义相应的运行类型,这并不理想。幸运的是,runtypes 能够像这样从我们的 runtype 派生出一个 TypeScript 类型。

import { Record, String, Number, Static } from 'runtypes'

const UserRuntype = Record({
    name: String,
    age: Number
})

type User = Static<typeof UserRuntype>  //equivalent to: type User = { name: string, age: number }

你已经拥有了它。你确实需要学习库的DSL,但至少你不需要定义两次类型!

完整的例子

让我们把这一切放在一起。

import { Record, String, Number, Static } from 'runtypes'

const parseJson = (str: string): unknown => JSON.parse(str)  //expell 'any'

const UserRuntype = Record({  //create a runtype
    name: String,
    age: Number
})

type User = Static<typeof UserRuntype>  //derive a TypeScript type from our runtype

const printUser = (user: User) => {
    console.log(`User ${user.name} is ${user.age} years old`)
}

const rawJson = `{
    "name": "Alice",
    "age": 31
}`
const user = parseJson(rawJson)  //the 'user' type is 'unknown'

if (UserRuntype.guard(user))
    printUser(user)  //'user' is refined by our guard to type 'User'

这似乎很难!

使用动态语言,如JavaScript,似乎更容易,但这只是把可能的类型错误推迟到运行时。你仍然需要注意你的数据结构。你在前期花时间用类型系统明确这个结构,在最初的开发和以后的开发中都会有收获。

类型断言的方法对动态数据进行类型化是低成本的,肯定比退回到动态类型化要好。2然后再加上一些运行时验证以获得更多的信心。

愉快的类型化!


  1. 还要确保你在你的tsconfig.json 中启用noImplicitAny 。(或者,更好的是,直接使用strict ,这将使你得到这个和其他一些更安全的默认值)。不幸的是,这不会捕捉到所有的any 的情况,例如当数据被明确地注释为any (如JSON.parse )。
  2. 如果你决定只走类型断言的路线,请确保你运行代码来验证你对类型的断言是否正确。更好的是,写一个测试!