14

对我来说,大多数时候,需要动态检查来验证获取响应。我在想,对于具有多个道具和附加检查的任何类型的对象,这是否可以通过用户定义的类型保护以通用方式完成,所以它可以使用如下:

打字稿游乐场

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

// ================= 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
4

5 回答 5

23

实际上有许多模块试图将 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: ❌</kbd> func: ❌</kbd> auto: ⚙️
Note: Auto-check marked as partial, since you can add decorators to class-methods (but not standalone functions) to have their argument types checked.

ts-运行时

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

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

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

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

核心特点: ts-base: ✔️ class: ✔️ func: ❌</kbd> auto: ❌</kbd>
Pro: Generates valid json-schemas, which have additional uses. (eg. can be used for data validation in other languages)
Con: Requires some manual work to write generated schemas to disk, package them to be available at runtime, and feed them to your selected JSON-schema validator.

io-ts(单独)

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

核心特点: ts-base: ❌</kbd> class: ❌</kbd> func: ❌</kbd> auto: ❌</kbd>
Pro: Doesn't require any typescript transformers, webpack plugins, or CLI commands to operate. (it uses "tricks" to infer the TS types from its custom type-definition structure)

io-ts-transformerio -ts 的扩展)

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

核心特点: ts-base: ✔️ class: ❌</kbd> func: ❌</kbd> auto: ❌</kbd>

ts-自动防护

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

核心特点: ts-base: ✔️ class: ❔</kbd> func: ❌</kbd> auto: ❌</kbd>
Con: You must add a specific js-doc tag to each interface you want a type-guard generated for. (a hassle, and error prone)

只输入

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

核心特点: ts-base: ✔️ class: ❔</kbd> func: ❌</kbd> auto: ❌</kbd>
Con: Cannot generate type-guards for generic types. (see here)

ts 类型检查

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

核心特点: ts-base: ✔️ class: ❔</kbd> func: ❌</kbd> auto: ❌</kbd>

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

免责声明

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

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

于 2020-09-30T11:07:30.567 回答
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);

在这里,我们导出了一些类型保护和组成现有类型保护的函数。, gString(), gNumber(),gBoolean()gNull()函数只是类型保护,而gObject(), gArray(), 和gUnion()使用现有的类型保护来制作新的类型保护。您可以看到如何gObject()获取一个充满类型保护属性的对象并创建一个新的类型保护,其中每个属性都根据相应的保护进行检查。您可以添加其他组合功能,例如gIntersection()or 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;

如果您眯着眼睛看,您会发现它类似于您的示例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!
}

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

Playground 代码链接

于 2019-12-26T02:36:07.643 回答
1

为了完成几乎详尽的jcalz 回答,在与外部 API 通信的情况下,我们可以使用生成的 TypeScript 客户端:强类型,或者没有类型保护,具体取决于生成器/技术,例如:

于 2020-01-29T09:25:31.280 回答
1

您可以使用类而不是类型并检查instanceOf

请检查示例

https://stackblitz.com/edit/types-in-runtime

我希望这可以帮助你

于 2019-12-25T20:01:26.703 回答
1

以下是如何使用 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

于 2019-12-26T18:10:56.080 回答