142

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?

2

11 Answers 11

60

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.gitbook.io/typescript/type-system/typeguard

Sign up to request clarification or add additional context in comments.

7 Comments

Interesting. Looks like something that could easily be generated automatically.
Yes, it could be automated, and indeed easily for common cases. The user defined guard however could do particular stuff like check an array's length or validate a string against a regex. Annotations per field would help but then i'd think it should be a class now rather than an interface.
@RichardForrester my question is "is there a way to use the type declaration to check the correctness of the object". This answer doesn't use the type declaration. Instead it requires writing tests which are fully redundant with the type declaration, which is precisely what I want to avoid.
Agreed! This answer is equivalent to writing your own validation function separate from the interface definition. What is the point of that?
Once you define the interface Test, you don't need any flexibility around "what is a Test". You have just defined it. It is an object with a property called "prop" which is a number. I shouldn't have to then write more Typeguard code to define what a Test is, right?
|
40

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.

Comments

30

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

3 Comments

This is no longer accurate. The typescript compiler flags experimentalDecorators and emitDecoratorMetadata allow recording of type information, see my answer for a library I wrote that uses this information at runtime.
This answer is strictly correct for the question as asked, but googlers should note that there are good solutions. Consider Alexy's suggestion for class validation (my preference), DS's recommendations of 3rd party interface builders, and teodor's suggestion of type guards.
The mentioned Issues has been closed as "Declined" and "Out of Scope" in July '23.
17

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

2 Comments

both these packages are awesome - saved me so much work! using the generated schemas in conjunction with ajv
7

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

4 Comments

Neat, any hope for getting something like this built into TypeScript. I thought that was something that AtScript did and Angular 2 needed.
Official TypeScript compiler does not cover (and probably never will) reflection because it's stated "out of scope". For me, it was a 10 day development effort, and I'm not in the core team: I had to learn a lot of things before becoming effective. One of the members of the TypeScript team could achieve this in a week or less. In a few words: there is nothing impossible or too hard about reflection implementation in TypeScript.
How can I use this isValid function? from what I can see it isn't exported from your package. It is exported from the validator file in the examples, but that isn't in the package. What's the best way to include it in my code?
@S.. the isValid function is just a demonstration of reflec-ts capabilities; if you want to build a full-blown type checker you should do it by yourself, or use other similar projects (but they cannot leverage reflection, at the moment). The main point here is not the type inspection, but the capability of the compiler to provide a way to reference an interface at runtime. Please note that reflec-ts project is currently in stand-by since I'm waiting for the transformer extensibility API in order to be decoupled from the compiler core.
7

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[];
}

Comments

4

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

Comments

3

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.

2 Comments

Just found this as was quite excited to use it. Shame however that it does not support async custom validators. This may prevent us being able to use it as we will have validators that may make DB calls to check if a provided value is unique for example.
@ste2425 Sorry, it was meant to provide schema validation, which is done on a different layer than the logic validation. For instance, a wrong range would return a 400 bad request before even going to the db - which is a part of the logic layer. I understand what you are trying to do - but that isn’t the design goal of this library.
3

You can use class-validation

  1. Replace interface with class.
    class Cat {
        @IsNotEmpty() name: string;
    }
    
    // Static typing works!
    const cat: Cat = { 
        name: "Barsik"
    };

  1. Create a validation function. Example:
    import { validateSync } from "class-validator";
    
    type data = {
        [key: string]: any;
    };
    
    // Create new class instance 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 instead of interface for static typing and class for validation
    if (validate(cat, Cat)) {
      // OK
    } else {
      // ERROR
    }

1 Comment

Thanks for this, but i keep getting this error "Cannot find name data"
2

To pile on the "use this lib" answers, here is mine: I've created a package called ts-data-checker which runs TypeScript language service at runtime to check JSON:

import { checker } from "ts-data-checker";

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

const { checkJson } = checker("PathPlannerConfig", "./nameofthisfile");

if (checkJson(`{ "nbMaxIter": 1 }`)) {
    console.log('valid!');
}

Comments

0

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.

2 Comments

Yes it's JSON, but my goal is to validate it at runtime using the already existing interface (or any other solution where I don't have to write the config structure twice)
You can validate that file at runtime (load it with ajax request and check whether it is valid or not). But let me explain one thing: interface is kind of schema for your javascript object, it is used only on compilation time. Yet it lacks too many informations to be used as validator. For example how would you write in your interface that some array shall contain minimum of three enum values? You have to write schema to have bigger flexibility of how your object have to look like. There are some generator online that build schema based on your json file like jsonschema.net

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.