Check if an object implements an interface at runtime with TypeScript

I load a JSON configuration file at runtime, and use an interface to define its expected structure:

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

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

interface DebugConfig {
    logLevel?: number;
}

...

This makes it convenient to access the various properties since I can use autocompletions etc.

Question: is there a way to use this declaration to check the correctness of the file I load? ie that I do not have unexpected properties?

Answers:

Answer

There "is" a way, but you have to implement it yourself. It's called a "User Defined Type Guard" and it looks like this:

interface Test {
    prop: number;
}

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

Of course, the actual implementation of the isTest function is totally up to you, but the good part is that it's an actual function, which means it's testable.

Now at runtime you would use isTest() to validate if an object respects an interface. At compile time typescript picks up on the guard and treats subsequent usage as expected, i.e.:

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
}

More in-depth explanations here: https://basarat.gitbooks.io/typescript/content/docs/types/typeGuard.html

Answer

No.

Currently, types are used only during development and compile time. The type information is not translated in any way to the compiled JavaScript code.

From https://stackoverflow.com/a/16016688/318557, as pointed out by @JasonEvans

There is an open issue since Jun 2015 about this in the TypeScript repo: https://github.com/microsoft/TypeScript/issues/3628

Answer

Here is another alternative, specifically for this:

ts-interface-builder is a tool you run at build time on your TypeScript file (e.g. foo.ts) to build runtime descriptors (e.g. foo-ti.ts).

ts-interface-checker uses these to validate objects at runtime. E.g.

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);

You can use strictCheck() method to ensure there are no unknown properties.

Answer

I suspect that TypeScript is (wisely) adhering to Curly's Law, and Typescript is a transpiler, not an object validator. That said, I also think that typescript interfaces would make for lousy object validation, because interfaces have a (wonderfully) limited vocabulary and can't validate against shapes that other programmers may use to distinguish objects, such as array length, number of properties, pattern properties, etc.

When consuming objects from non-typescript code, I use a JSONSchema validation package, such as AJV, for run-time validation, and a .d.ts file generator (such as DTSgenerator or DTS-generator) to compile TypeScript type definitions from my JSONshcema.

The major caveat is that because JSONschemata are capable of describing shapes that cannot be distinguished by typescript (such as patternProperties), it's not a one-to-one translation from JSON schema to .t.ds, and you may have to do some hand editing of generated .d.ts files when using such JSON schemata.

That said, because other programmers may use properties like array length to infer object type, I'm in the habit of distinguishing types that could be confused by the TypeScript compiler using enum's to prevent the transpiler from accepting use of one type in place of the other, like so:

[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

Which generates a .d.ts file like so:

[MyTypes.d.ts]

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

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

Yes. You can do this check at runtime by using an enhanced version of the TypeScript compiler that I released a few time ago. You can do something like the following:

export interface Person {
    name: string;
    surname: string;
    age: number;
}

let personOk = { name: "John", surname: "Doe", age: 36 };
let personNotOk = { name: 22, age: "x" };

// YES. Now you CAN use an interface as a type reference object.
console.log("isValid(personOk):  " + isValid(personOk, Person) + "\n");
console.log("isValid(personNotOk):  " + isValid(personNotOk, Person) + "\n");

and this is the output:

isValid(personOk):  true

Field name should be string but it is number
isValid(personNotOk):  false

Please note that the isValid function works recursively, so you can use it to validate nested objects, too. You can find the full working example here

Answer

yes, there is a lib that does it https://github.com/gcanti/io-ts

the idea is simple, have simple checks for properties composed into more complex checks for objects

Answer

Here's a good way. You can convert a TypeScript interface to JSON schema using typescript-json-schema, e.g.

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

Then validate data at runtime using a JSON schema validator such as ajv, e.g.

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);
}
Answer

I don't know how your configuration file looks like, but most obvious would be json file, though I would go with json schema to validate if file fits the schema or not.

Here's json schema v4 documentation: http://json-schema.org/documentation.html

And one of examples how you could test it: https://github.com/fge/json-schema-validator

Of course you have to write your schema based on interfaces, but you can't use them directly.

Answer

You can use class-validation

  1. Replace interface to class.
    class Cat {
        @IsNotEmpty() name: string;
    }

    // Static typing is work !!!
    const cat: Cat = { 
        name: "Barsik"
    };

  1. Create validation function. Example:
    import { validateSync } from "class-validator";

    type data = {
        [key: string]: any;
    };

    // Create new class instance, fill it and validate via "class-validator"
    export const validate = <D extends data, C extends {new(): D}>
      (data: D, classTemplate: C): boolean => {
        const instanceClass = new classTemplate();
        Object.keys(data).forEach((key) => {
            instanceClass[key] = data[key];
        });
        return !validateSync(instanceClass).length;
    }

  1. Use class as interface for static typing and class for validation
    if (validate(cat, Cat)) {
      // OK
    } else {
      // ERROR
    }

Answer

I realize this question is old, but I just wrote my own validator for JSON objects and typescript, for this exact purpose, using decorators.
Available here: ts-json-object.
Typescript has moved on a bit since this question was asked, and now has experimental features allowing recording of type information for later usage.
The following example validates @required and @optional properties, but also validates their type, even though there is no mentioning of the type in the validation notation.

Example:

import {JSONObject,required,optional,lt,gte} from 'ts-json-object'

class Person extends JSONObject {
    @required // required
    name: string
    @optional // optional!
    @lt(150) // less than 150
    @gte(0) // Greater or equal to 0
    age?: number
}

let person = new Person({
 name: 'Joe'
}) // Ok
let person = new Person({
}) // Will throw a TypeError, because name is required
let person = new Person({
 name: 123
}) // Will throw a TypeError, because name must be a string

Has many other features such as custom validations, etc.

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.