打字稿在运行时按类型或接口检查对象,并在 2020 年以上使用打字机

RTW*_*RTW 11 typescript

大多数时候对我来说,需要动态检查来验证 fetch 响应。我在想,这可以用用户定义的 typeguard 以通用方式完成,用于具有多个 props 和附加检查的任何类型的对象,因此可以使用以下内容:

打字稿游乐场

这是一个带有示例对象的示例,但我想要一个没有它的函数。

// ================= 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
Run Code Online (Sandbox Code Playgroud)

Ven*_*ryx 13

实际上有许多模块试图将 TypeScript 类型信息转换为可用于验证数据结构的运行时信息。

我将尝试在这里列出和比较各种解决方案。(根据我估计它们的有效性/通用性大致排序;是的,这有点主观!)

核心功能:(用??yes、?no、??partial、?unknown 标记)

ts-baseTS 基础:标准 TypeScript 类型用作生成类型保护的基础。(而不是相反)
class类:可以为类(基于形状,而不是instanceof)生成类型保护,而不仅仅是接口。(可能的解决方法:interface ClassX_I extends ClassX {}
func功能:可以生成的函数的类型定义的运行时信息。
auto自动检查:可以自动生成对生成的类型保护的调用。

解决方案

打字稿是

GitHub: 500 NPM: 2,555 (2020-09-30)

核心功能: ts-base: ?? class: ? func: ? auto: ??
注意:自动检查标记为部分,因为您可以将装饰器添加到类方法(但不是独立函数)以检查它们的参数类型。

ts 运行时

GitHub: 313 NPM: 96 (2020-09-30)

核心特性: ts-base: ?? class: ?? func: ?? auto: ??
Con:目前不能仅应用于特定文件或功能;它在整个项目中添加了类型保护调用。(但PR 似乎很受欢迎
Con:包含注释:“这个包仍然是实验性的,生成的代码不打算用于生产。它是一个概念证明......”

typescript-json-schema(+模式验证器,例如ajv

GitHub: 1,400 NPM: 51,664 (2020-09-30)

核心特性: ts-base: ?? class: ?? func: ? auto: ?
Pro:生成有效的 json-schemas,有额外的用途。(例如,可用于其他语言的数据验证)
缺点:需要一些手动工作将生成的模式写入磁盘,将它们打包以在运行时可用,并将它们提供给您选择的 JSON 模式验证器。

io-ts(单独)

GitHub: 3,600 NPM: 296,577 (2020-09-30)

核心特性: 优点ts-base: ? class: ? func: ? auto: ?
不需要任何打字稿转换器、webpack 插件或 CLI 命令来操作。(它使用“技巧”从其自定义类型定义结构中推断出 TS 类型)

IO-TS-变压器(扩展IO-TS

GitHub: 16 NPM: 7 (2020-09-30)

核心功能: ts-base: ?? class: ? func: ? auto: ?

ts-auto-guard

GitHub: 134 NPM: 46 (2020-09-30)

核心功能: ts-base: ?? class: ? func: ? auto: ?
缺点:您必须为每个要为其生成类型保护的接口添加一个特定的 js-doc 标签。(麻烦且容易出错)

只打字

GitHub: 25 NPM: 101 (2020-09-30)

核心特性: ts-base: ?? class: ? func: ? auto: ?
缺点:无法为泛型类型生成类型保护。(见这里

ts 类型检查

GitHub: 13 NPM: 3 (2020-09-30)

核心功能: ts-base: ?? class: ? func: ? auto: ?

尚未评估ts-json-schema-generatortypescript-to-json-schema
排除(无公共存储库):typescript-runtime-types

免责声明

我不是列出的任何解决方案的创建者或维护者。我创建该列表是为了帮助开发人员根据一组一致的标准比较各种解决方案,同时添加有用的信息,例如 GitHub 星标和 NPM 每周下载量。(欢迎编辑以定期更新这些值——但请记住更改上次更新时间标签!)

对于那些有足够声誉的人,请随意添加您遇到的其他解决方案。(尽管请尽量使新条目的文本与现有条目保持一致)


jca*_*alz 10

TypeScript 的类型系统在编译为 JavaScript 时会被删除。这意味着任何使用标准tsc编译器本身从typeinterface定义生成运行时类型保护的努力都不会成功;在运行时没有任何这些定义可供您使用。所以ofType<T>()无法实施。

那么,什么可以做什么?


如果您愿意在构建系统中使用其他一些编译步骤,您可以编写或使用一个转换器,在这些定义被擦除之前为您制作类型保护。例如,typescript-is会这样做。


或者你可以改用class定义;这使得在运行时检查很容易(只需使用instanceof),但困难的部分是将 JSON 反序列化为类实例并在反序列化时捕获错误,而无需自己手动编写。所有这一切确实是从实现移动你的问题ofType<Book>(someObj)来实施myDeserializerFunction(Book, someObj),其中Book一类的构造函数。

这里至少可以使用装饰器类元数据来生成程序化反序列化所需的代码。您可以自己编写,也可以使用现有的库,例如json2typescript.


最后,您可能决定从类型保护开始,让 TypeScript从中推断出您的type定义。也就是说,不是定义Book并希望从中获得类型保护bookGuard(),而是编写类型保护bookGuard()Book根据typeof bookGuard.

这种类型保护可以通过将现有的更简单的类型保护组合在一起来构建,因此它看起来更像是一个声明性类型定义而不是数据检查函数。您可以自己编写,也可以使用现有的库,例如io-ts.

对于这种方法,看看如何编写这样一个库是有益的。这是一种可能的实现:

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);
Run Code Online (Sandbox Code Playgroud)

在这里,我们导出了一些组成现有类型防护的类型防护和函数。在gString()gNumber()gBoolean(),和gNull()功能都只需键入的狱警,而gObject()gArray()以及gUnion()利用现有型后卫,使新型警卫了出来。您可以看到如何gObject()获取一个充满类型保护属性的对象并创建一个新的类型保护,其中每个属性都根据相应的保护进行检查。您可以添加其他组合函数,例如gIntersection()gPartial(),但这里的函数对于您的示例来说已经足够了。

现在你的BookWriter定义看起来像这样(假设上面已经作为 namespace 导入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;
Run Code Online (Sandbox Code Playgroud)

如果你眯着眼睛看它,你会发现它类似于你的例子WriterBook定义。但在我们的例子中,基本对象是类型保护gWritergBook类型,Writer并且Book是从它们派生的。然后你可以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!
}
Run Code Online (Sandbox Code Playgroud)

好的,希望有帮助;祝你好运!

Playground 链接到代码