检查对象是否在运行时使用 TypeScript 实现接口

IT技术 javascript typescript
2021-01-29 08:52:40

我在运行时加载一个 JSON 配置文件,并使用一个接口来定义其预期结构:

interface EngineConfig {
    pathplanner?: PathPlannerConfig;
    debug?: DebugConfig;
    ...
}

interface PathPlannerConfig {
    nbMaxIter?: number;
    nbIterPerChunk?: number;
    heuristic?: string;
}

interface DebugConfig {
    logLevel?: number;
}

...

这使得访问各种属性变得方便,因为我可以使用自动完成等。

问题:有没有办法使用这个声明来检查我加载的文件的正确性?即我没有意外的属性?

6个回答

“有”一种方法,但您必须自己实施。它被称为“用户定义的类型保护”,它看起来像这样:

interface Test {
    prop: number;
}

function isTest(arg: any): arg is Test {
    return arg && arg.prop && typeof(arg.prop) == 'number';
}

当然,isTest函数的实际实现完全取决于你,但好的部分是它是一个实际的函数,这意味着它是可测试的。

现在在运行时,您将使用isTest()验证对象是否遵守接口。在编译时,typescript会受到保护,并按预期处理后续使用,即:

let a:any = { prop: 5 };

a.x; //ok because here a is of type any

if (isTest(a)) {
    a.x; //error because here a is of type Test
}

更深入的解释在这里:https : //basarat.gitbook.io/typescript/type-system/typeguard

@Phil 这不是等价的,它正是这样的:哈哈 :) 关键是您在定义这样的对象时有更大的灵活性。它还可以指示代码完成工具关于对象是什么类型,因此您可能会在某些情况下获得代码完成,否则您将无法完成。如果您需要其中任何一个,那么这可能是一个可以接受的折衷方案,因为唯一的实际答案是“对不起,没有做不到”。
2021-03-25 08:52:40
@RichardForrester 我的问题是“有没有办法使用类型声明来检查对象的正确性”。此答案不使用类型声明。相反,它需要编写与类型声明完全冗余的测试,这正是我想要避免的。
2021-03-28 08:52:40
同意!这个答案相当于编写自己的验证函数,与接口定义分开。那有什么意义呢?
2021-03-29 08:52:40
是的,它可以自动化,而且对于常见情况确实很容易。然而,用户定义的守卫可以做一些特定的事情,比如检查数组的长度或根据正则表达式验证字符串。每个字段的注释会有所帮助,但我认为它现在应该是一个类而不是一个接口。
2021-04-08 08:52:40
有趣的。看起来很容易自动生成的东西。
2021-04-11 08:52:40

不。

目前,类型仅在开发和编译期间使用。类型信息不会以任何方式转换为已编译的 JavaScript 代码。

正如@JasonEvans 所指出的,来自https://stackoverflow.com/a/16016688/318557

自 2015 年 6 月以来,TypeScript 存储库中有一个关于此的未解决问题:https : //github.com/microsoft/TypeScript/issues/3628

这个答案对于所问的问题是严格正确的,但谷歌人应该注意有很好的解决方案。考虑 Alexy 对类验证的建议(我的偏好)、DS 对 3rd 方界面构建器的建议以及 teodor 对类型保护的建议。
2021-03-18 08:52:40
这不再准确。typescript编译器标记experimentalDecoratorsemitDecoratorMetadata允许记录类型信息,请参阅我编写的库的答案,该库在运行时使用此信息。
2021-04-09 08:52:40

这是另一种选择,专门用于此:

ts-interface-builder是您在构建时在 TypeScript 文件(例如foo.ts)上运行以构建运行时描述符(例如foo-ti.ts)的工具。

ts-interface-checker使用这些在运行时验证对象。例如

import {createCheckers} from 'ts-interface-checker';
import fooDesc from 'foo-ti.ts';
const checkers = createCheckers(fooDesc);

checkers.EngineConfig.check(someObject);   // Succeeds or throws an informative error
checkers.PathPlannerConfig.check(someObject);

您可以使用strictCheck()方法来确保没有未知的属性。

这是一个好方法。您可以使用typescript-json-schema将 TypeScript 接口转换为 JSON模式,例如

typescript-json-schema --required --noExtraProps \
  -o YOUR_SCHEMA.json YOUR_CODE.ts YOUR_INTERFACE_NAME

然后在运行时使用 JSON 模式验证器(例如ajv )验证数据,例如

const fs = require('fs');
const Ajv = require('ajv');

// Load schema
const schema = JSON.parse(fs.readFileSync('YOUR_SCHEMA.json', {encoding:"utf8"}));
const ajv = new Ajv();
ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-04.json'));
var validator = ajv.compile(schema);

if (!validator({"hello": "world"})) {
  console.log(validator.errors);
}
2021-04-11 08:52:40
这两个包都很棒 - 为我节省了很多工作!将生成的模式与 ajv 结合使用
2021-04-12 08:52:40

我怀疑 TypeScript 是(明智地)遵守 Curly 定律,而 Typescript 是一个转译器,而不是一个对象验证器。也就是说,我还认为 typescript 接口会导致糟糕的对象验证,因为接口的词汇量(非常)有限,并且无法针对其他程序员可能用来区分对象的形状进行验证,例如数组长度、属性数量、图案属性等。

当消耗来自非typescript代码对象,我使用了JSONSchema验证包,如AJV,用于运行时间验证,和一个.d.ts文件发生器(如DTSgeneratorDTSgenerator)从编译打字原稿类型定义我JSONshcema。

主要的警告是,因为 JSONschemata 能够描述无法通过typescript(例如patternProperties区分的形状,它不是从 JSON 模式到 .t.ds 的一对一转换,您可能需要做一些手工使用此类 JSON 模式时编辑生成的 .d.ts 文件。

也就是说,因为其他程序员可能使用数组长度之类的属性来推断对象类型,所以我习惯于区分可能被 TypeScript 编译器使用枚举混淆的类型,以防止转译器接受使用一种类型来代替其他,像这样:

[MyTypes.yaml]

definitions: 
    type-A: 
        type: object
        properties:
            type:
                enum:
                - A
            foo: 
                type: array
                item: string
                maxLength: 2
    type-B: 
        type: object
        properties:
            type:
                enum:
                - B
            foo: 
                type: array
                item: string
                minLength: 3
        items: number

它会生成一个.d.ts像这样文件:

[MyTypes.d.ts]

interface typeA{
    type: "A";
    foo: string[];
}

interface typeB{
    type: "B";
    foo: string[];
}