TypeScript API返回数据类型检查

245 阅读4分钟

当使用ajax等获取JSON数据时,如何对后端返回的数据做类型安全检查?

检查数据类型有2种方法:

  1. 静态类型检查(编译时静态分析)
  2. 代码运行时检查

静态类型检查需要我们提前准备好要检查的数据代码。但是在一些场景中我们不能获取到,比如通过API、文件或用户输入的数据。

如果想100%类型安全,我们必须在运行时分析解析数据。大多数类型检查语言包含静态检查和运行时检查,TypeScript仅仅使用了静态检查。TypeScript编译后生成的Javascript没有保留任何有关类型的任何信息,因此获取到的动态数据(如API返回数据)不能够做一个安全检查。

例子

下面看通过API获取数据的例子:

const fetchData = async (request: Request): any => {
   ...
   return await response.json()
}


const article = await fetchData(...)
article. // no code completion

假设返回的数据类型期望是:

type Article = {
  title: string
  views: number
}

接下来使用强制类型转换检查API返回的数据:

const article = await fetchData(...) as Article
article. // code completion works!

以上实现了API返回的数据的类型检查,我们仍可以改进上面的代码,使代码能够更加通用。

const fetchData = async <T>(request: Request): Promise<T> => {
   ...
   return await response.json()
}

const article = await fetchData(...)
article. // code completion works

现在看我们的代码是非常优美的并且可以任何其他的地方去使用。如果返回的API数据和期望的数据类型不一致,也不会有报错警告。因为在TypeScript中没有代码运行的时候类型检查。

如何在运行时检查数据类型

在TS中没有运行时检查这个功能,我们需要手动去检查。对基本类型的数据检查比较简单,如string或number类型。

if (typeof myVariable === 'number') {
  // do our stuff
}if (typeof myVariable === 'string') {
  // do our stuff
}

object检查比较复杂些。需要检查是不是对象,对象是不是null,对象是否有某个属性以及每个属性单独的类型检查。上面Article类型检查如下:

if (
  typeof article === 'object' && article !== null &&
  hasOwnProperty(article, 'title') && typeof article.title === 'string' &&
  hasOwnProperty(article, 'views') in article && typeof article.views === 'number'
) {
  // do our stuff
}

类型检测函数

把类型检测封装成一个函数,函数判断传入的参数是否是期望的数据类型。传入一个数据返回一个boolean值。

const isArticle = (article: any): article is Article =>
    typeof article === 'object' && article !== null &&
    hasOwnProperty(article, 'title') && typeof article.title === 'string' &&
    hasOwnProperty(article, 'views') in article && typeof article.views === 'number';

// TS does not know that this object has type Article
const article = {
    title: 'How to train a dragon',
    views: 73
}

article. // no code completion

if (isArticle(article)) {
    article. // code completion works
    // we can be sure that article has correct type even ar runtime
}

但是还有一个问题是如何让定义的数据类型同步?

根据运行时类型推断TS类型

在上面的例子中我们使用类型检测函数,来判断数据类型。还有一种方式是使用断言函数。如果传入的参数不是指定的数据类型,断言函数抛出一个异常而不是返回一个boolean值。

const assertString(param: any): asserts param is string {
  if (typeof param !== "string") {
    throw new AssertionError("Not a string!");
  }
}

const usageOfAbove = (val: any) => {
  assertString(val);
  
  // Now TypeScript knows that 'val' is a 'string'.
 }

断言函数的好处是通过抛出的异常信息我们能够知道问题错在了哪里。

基本类型的检测:

type TypeGuard<T> = (val: unknown) => T;

const string: TypeGuard<string> = (val: unknown) => {
    if (typeof val !== 'string') throw new Error();
    return val;
}

const number: TypeGuard<number> = (val: unknown) => {
    if (typeof val !== 'number') throw new Error();
    return val;
}

数据类型的检测,检测数组的每个元素的数据类型:

const array = <T>(inner: TypeGuard<T>) => (val: unknown): T[] => {
    if (!Array.isArray(val)) throw new Error();
    return val.map(inner);
}

数据类型检测函数:

const object = <T extends Record<string, TypeGuard<any>>>(inner: T) => {
    return (val: unknown): { [P in keyof T]: ReturnType<T[P]> } => {
        if (val === null || typeof val !== 'object') throw new Error();

        const out: { [P in keyof T]: ReturnType<T[P]> } = {} as any;

        for (const k in inner) {
            out[k] = inner[k]((val as any)[k])
        }

        return out
    }
}

使用:

const Article = object({
    title: string,
    views: number,
    comments: array(object({
        content: string,
        author: string,
        upvotes: number
    }))
});

我们在运行的时候指定了数据类型,使用typeof操作符能够把运行时的数据类型同步到TS数据类型。

type Article = ReturnType<typeof Article>

运用到fecth data

const fetchData = async (request: Request): Promise<unknown> => {
   ...
   return await response.json()
}
  
const Article = object({
    title: string,
    views: number
});
type Article = ReturnType<typeof Article>

const article: Article = Article(await fetchData(...))
article. // code completion works and it's type-safe!

如果您想要一个比上面更健壮的方案,这里有一些具有此功能的库。

参考文献:

  1. How to correctly use TypeScript types for your API response
  2. Promises
  3. Typeof Type Operator
  4. Utility Types