typescript 在2020+中,使用typeguard在运行时按类型或接口检查对象

mrwjdhj3  于 2023-01-10  发布在  TypeScript
关注(0)|答案(5)|浏览(130)

对我来说,大多数时候需要动态检查来验证获取响应。我在想,这是否可以通过用户定义的typeguard以通用的方式来完成,用于任何类型的具有多个属性和附加检查的对象,因此它可以像这样使用:
打字操场。
Here is an example with sample object, but i want a function without it.

// ================= shared exported =================
type Writer = {
  name: string
  age: number
}

type Book = {
  id: number
  name: string
  tags: string[] | null
  writers: Writer[]
}

// function to check object with multiple props general shape, to not do it by hand
function ofType<T>(obj: any): obj is T {
  if (!obj) return false;

   // how to?
   return true // or false 
}

// ================= used and defined in components =================
function isBook(obj: any): obj is Book {
  if (!ofType<Book>(obj)) return false //checking for shape and simple types

  // cheking for specific values and ranges
  if (obj.id < 1) return false 
  if (obj.writers && obj.writers.some(( { age } )=> age < 5 || age > 150)) return false 

  return true
}

const book = {
  id: 1,
  name: 'Avangers',
  tags: ['marvel', 'fun'],
  writers: [ {name: 'Max', age: 25}, {name: 'Max', age: 25}]
}

console.log(isBook(book)) // true or false
iovurdzv

iovurdzv1#

实际上有许多模块试图将TypeScript类型信息转换为可用于验证数据结构的运行时信息。
我将尝试在这里列出并比较各种解决方案。(大致按照我估计它们的有效性/通用性排序;是的,这有点主观!)

Core features: (marked with ✔️yes, ❌no, ⚙️partial, ❔unknown)

    • ts-base TS base:**标准TypeScript类型用作类型元数据的基础。(反之亦然)
    • class Classes:*可以为类生成类型元数据(基于shape, 而不是 * instanceof),而不仅仅是接口。
    • func函数:**可以为函数生成类型元数据。
    • guard类型保护:**包括根据类型元数据验证运行时数据的函数。(即类型保护)
    • auto Auto-check:**可以自动生成对包含的类型保护的调用。

溶液

    • 第一电子第一电子第一电子:500****第一电子第二电子:2,555**(2020年9月30日)
      **Core features:**ts-base: ✔️class : ❌func: ❌guard: ✔️auto: ⚙️
    • 注意:**Auto-check标记为partial,因为您可以向类方法(但不是独立函数)添加装饰器来检查它们的参数类型。

typescript-json-schema(+架构验证器,例如ajv

    • 一个电子六个电子一个电子:1 400个****一个电子七个电子:51,664**(2020年9月30日)
      **Core features:**ts-base: ✔️class: ✔️func: ❌guard: ❌auto: ❌
    • Pro:**生成有效的json-schema,它有其他用途。(例如,可用于其他语言中的数据验证)
    • 缺点:**需要一些手工工作来将生成的模式写入磁盘,将它们打包以便在运行时可用,并将它们提供给选定的JSON模式验证器。

∮ ∮ ∮ ∮ ∮

    • 一个月9次飞行:54次****一个月10次飞行:648**(2022年3月9日)
      **Core features:**ts-base: ✔️class: ✔️func: ✔️guard: ❌auto: ❌
    • Pro:**提供关于TypeScript类型的丰富元数据,可用于类型保护之外的功能。

∮ ∮ ∮ ∮ ∮

    • GitHub:77****NPM:79**(2022年3月9日)
      **Core features:**ts-base: ✔️class: ✔️func: ✔️guard: ❌auto: ❌
    • Pro:**提供关于TypeScript类型的丰富元数据,可用于类型保护之外的功能。

∮ ∮ ∮ ∮ ∮

    • GitHub:313****NPM:96**(2020年9月30日)
      **Core features:**ts-base: ✔️class: ✔️func: ✔️guard: ✔️auto: ✔️
    • 缺点:**当前不能仅应用于特定文件或函数;它在整个项目中添加了类型保护调用。(但是PRs appear welcomed
    • Con:**包含注解:"此软件包仍处于试验阶段,生成的代码不打算用于生产。它是概念验证..."

(单独给药)

    • GitHub:3,600****x一个20英尺一个:296,577**(2020年9月30日)
      **Core features:**ts-base: ❌class: ❌func: ❌guard: ✔️auto: ❌
    • 优点:**不需要任何类型脚本转换器、webpack插件或CLI命令来操作。(它使用"技巧"从其自定义类型定义结构推断TS类型)

io-ts-transformer(用于io-ts的扩展)

    • GitHub:16****NPM:7**(2020年9月30日)
      **Core features:**ts-base: ✔️class: ❌func: ❌guard: ✔️auto: ❌

    • GitHub:134****NPM:46**(2020年9月30日)
      **Core features:**ts-base: ✔️class: ❔func: ❌guard: ✔️auto: ❌
    • 缺点:**您必须为每个想要生成type-guard的接口添加一个特定的js-doc标记。(这很麻烦,而且容易出错)

∮ ∮ ∮ ∮ ∮

    • GitHub:25****NPM:101**(2020年9月30日)
      **Core features:**ts-base: ✔️class: ❔func: ❌guard: ✔️auto: ❌
    • 缺点:**无法为泛型类型生成类型保护。(请参阅此处)

免责声明

我不是这里列出的任何一个解决方案的创建者或维护者,我创建这个列表是为了帮助开发者在一组一致的标准上比较不同的解决方案,同时添加一些有用的信息,比如GitHub星和NPM每周下载量。(欢迎定期编辑这些值以保持最新--尽管记住要修改最后更新时间标签!)
对于那些有足够声誉的人,请随意添加您遇到的其他解决方案。(尽管请尽量保持新条目的文本与现有条目一致)

xtfmy6hx

xtfmy6hx2#

TypeScript的类型系统在编译为JavaScript时被擦除,这意味着任何使用标准tsc编译器自身从typeinterface定义生成运行时类型保护的努力都不会成功;在运行时,这些定义中没有任何东西可以使用,因此ofType<T>()无法实现。
那你能做什么呢?
如果你愿意在你的构建系统中使用一些其他的编译步骤,你可以编写或使用一个转换器,在这些定义被擦除之前为你提供类型保护,例如typescript-is就可以做到这一点。
或者可以使用class定义;这使得在运行时检查变得很容易(只需要使用instanceof),但是困难的部分是将JSON反序列化为一个类示例,并在反序列化时捕获错误,而不需要自己手动编写。所有这一切都是将问题从实现ofType<Book>(someObj)转移到实现myDeserializerFunction(Book, someObj),其中Book是一个类构造函数。
在这里,至少你可以使用装饰器和类元数据来生成编程反序列化所需的代码,你可以自己编写,或者使用现有的库,比如json2typescript
最后,您可能决定从类型保护开始,并让TypeScript从它们 * 推断 * 您的type定义,也就是说,不是定义Book并希望从中获得类型保护bookGuard(),而是编写类型保护bookGuard()并根据typeof bookGuard定义Book
这个类型保护可以通过组合现有的更简单的类型保护来构建,所以它看起来更像是一个声明性的类型定义,而不是一个数据检查函数。你可以自己编写,或者使用现有的库,比如io-tszod
对于这种方法,了解一下如何编写这样的库是很有启发性的。下面是一个可能的实现:

export type Guard<T> = (x: any) => x is T;
export type Guarded<T extends Guard<any>> = T extends Guard<infer V> ? V : never;
const primitiveGuard = <T>(typeOf: string) => (x: any): x is T => typeof x === typeOf;
export const gString = primitiveGuard<string>("string");
export const gNumber = primitiveGuard<number>("number");
export const gBoolean = primitiveGuard<boolean>("boolean");
export const gNull = (x: any): x is null => x === null;
export const gObject =
    <T extends object>(propGuardObj: { [K in keyof T]: Guard<T[K]> }) =>
        (x: any): x is T => typeof x === "object" && x !== null &&
            (Object.keys(propGuardObj) as Array<keyof T>).
                every(k => (k in x) && propGuardObj[k](x[k]));
export const gArray =
    <T>(elemGuard: Guard<T>) => (x: any): x is Array<T> => Array.isArray(x) &&
        x.every(el => elemGuard(el));
export const gUnion = <T, U>(tGuard: Guard<T>, uGuard: Guard<U>) =>
    (x: any): x is T | U => tGuard(x) || uGuard(x);

这里我们导出了一些类型保护和函数,它们组成了现有的类型保护。gString()gNumber()gBoolean()gNull()函数只是类型保护,而gObject()gArray()、和gUnion()利用现有类型的防护装置来制造新型防护装置。您可以看到gObject()如何获取一个充满类型保护属性的对象,并创建一个新的类型保护,其中每个属性都根据相应的保护进行检查。您可以添加其他复合函数,如gIntersection()gPartial(),但这里的函数对于您的示例来说已经足够了。
现在你的BookWriter定义如下所示(假设上面的定义已经作为命名空间G导入):

const _gWriter = G.gObject({
    name: G.gString,
    age: G.gNumber,
});
interface Writer extends G.Guarded<typeof _gWriter> { }
const gWriter: G.Guard<Writer> = _gWriter;

const _gBook = G.gObject({
    id: G.gNumber,
    name: G.gString,
    tags: G.gUnion(G.gArray(G.gString), G.gNull),
    writers: G.gArray(gWriter)
})
interface Book extends G.Guarded<typeof _gBook> { }
const gBook: G.Guard<Book> = _gBook;

如果你仔细看一下,你会发现它类似于你的示例WriterBook定义,但是在我们的例子中,基本对象是类型保护gWritergBook,类型WriterBook是从它们派生出来的,然后你可以直接使用gBook来代替不存在的ofType<Book>()

const book = JSON.parse('{"id":1,"name":"Avangers","tags":["marvel","fun"],' +
    '"writers":[{"name":"Max","age":25},{"name":"Max","age":25}]}');

if (gBook(book)) {
    console.log(book.name.toUpperCase() + "!"); // AVANGERS!
}

好吧,希望能有所帮助;祝你好运!
Playground代码链接

fjaof16o

fjaof16o3#

您可以使用类代替类型,并检查instanceOf
请检查示例
https://stackblitz.com/edit/types-in-runtime
希望这能帮到你

jq6vz3qz

jq6vz3qz4#

下面是如何使用TypeOnly

安装typeonly@typeonly/checker

# Used at build time
npm i -D typeonly

# Used at runtime
npm i @typeonly/checker

package.json文件中,添加typeonly命令。例如,假设TypeScript配置为在dist/目录中输出:

"build": "npm run typeonly && npm run tsc",
    "typeonly": "typeonly --bundle dist/book.to.json src/book.d.ts",
    "tsc": "tsc"

在代码中,将这些类型放在单独的定义文件中:

// src/book.d.ts

type Writer = {
  name: string
  age: number
}

type Book = {
  id: number
  name: string
  tags: string[] | null
  writers: Writer[]
}

然后,在代码中导入类型和检查器:

import { createChecker } from "@typeonly/checker";
import { Book } from "./book";

const checker = createChecker({
  bundle: require("./book.to.json")
});

function ofType(obj: any, typeName: "Book"): obj is Book
function ofType(obj: any, typeName: string): boolean {
  if (!obj) return false;
  return checker.check("./book", typeName, obj).valid
}

function isBook(obj: any): obj is Book {
  if (!ofType(obj, "Book")) return false //checking for shape and simple types

  // cheking for specific values and ranges
  if (obj.id < 1) return false 
  if (obj.writers && obj.writers.some(( { age } )=> age < 5 || age > 150)) return false 

  return true
}

const book = {
  id: 1,
  name: 'Avangers',
  tags: ['marvel', 'fun'],
  writers: [ {name: 'Max', age: 25}, {name: 'Max', age: 25}]
}

console.log(isBook(book)) // true

npm run build构建,那么它应该可以工作。
另请参阅:https://github.com/tomko-team/typeonly

b1payxdu

b1payxdu5#

为了完成几乎详尽的jcalz' answer,在与外部API通信的情况下,我们可以使用生成的TypeScript客户端:强类型,带ou但不带typeguard,取决于生成器/技术,例如:

  • autorest,使用REST API,使用OpenAPI规范格式
  • gRPC及其实验性TypeScript支持

相关问题