138

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?

MasterScrat
  • 7,090
  • 14
  • 48
  • 80
  • 2
    For reference : [Detect whether object implement interface in TypeScript dynamicaly](http://stackoverflow.com/questions/16013667/detect-whether-object-implement-interface-in-typescript-dynamicaly) – Jason Evans Nov 19 '15 at 16:00
  • Possible duplicate of [Interface type check with Typescript](https://stackoverflow.com/questions/14425568/interface-type-check-with-typescript) – Krisztián Balla Nov 24 '17 at 08:05

11 Answers11

62

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

Vispercept
  • 105
  • 1
  • 9
Teodor Sandu
  • 1,348
  • 1
  • 20
  • 31
  • 27
    Interesting. Looks like something that could easily be generated automatically. – MasterScrat Apr 07 '17 at 13:49
  • 1
    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. – Teodor Sandu Apr 12 '17 at 09:07
  • 26
    @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. – MasterScrat Feb 11 '19 at 22:44
  • Agreed! This answer is equivalent to writing your own validation function separate from the interface definition. What is the point of that? – Phil Apr 27 '21 at 09:00
  • @Phil it's not equivalent, it's exactly that lol :) the point is you have more flexibility in defining what such an object is. Also it can instruct code completion tools as to what type an object is, so you may get code completion in some scenarios where you otherwise wouldn't. If you need any of these, then it might be an acceptable compromise given that the only actual answer is "sorry, no can't do". – Teodor Sandu May 05 '21 at 12:31
  • 2
    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? – Phil May 06 '21 at 17:22
  • @Phil wrong. There are more complex scenarios than number, which could benefit from this alternative. – Teodor Sandu May 19 '21 at 12:25
35

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.

DS.
  • 22,632
  • 6
  • 47
  • 54
27

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

MasterScrat
  • 7,090
  • 14
  • 48
  • 80
  • 2
    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. – Moshe Gottlieb Mar 09 '20 at 22:03
  • 1
    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. – jtschoonhoven Apr 09 '20 at 19:10
16

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);
}
DS.
  • 22,632
  • 6
  • 47
  • 54
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

pcan
  • 893
  • 11
  • 24
  • 1
    Neat, any hope for getting something like this built into TypeScript. I thought that was something that AtScript did and Angular 2 needed. – jpierson Aug 25 '16 at 14:11
  • 7
    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. – pcan Aug 25 '16 at 14:35
  • 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.. May 09 '19 at 11:23
  • 1
    @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. – pcan May 09 '19 at 12:13
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[];
}
Jthorpe
  • 9,756
  • 2
  • 49
  • 64
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

Trident D'Gao
  • 18,973
  • 19
  • 95
  • 159
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.

Moshe Gottlieb
  • 3,963
  • 25
  • 41
  • 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 Apr 01 '22 at 13:38
  • 2
    @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. – Moshe Gottlieb Jul 01 '22 at 12:50
2

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
    }

akaRem
  • 7,326
  • 4
  • 29
  • 43
1

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!');
}
Markus Johnsson
  • 3,949
  • 23
  • 30
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.

Maciej Kwas
  • 6,169
  • 2
  • 27
  • 51
  • 1
    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) – MasterScrat Nov 19 '15 at 10:38
  • 1
    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 http://jsonschema.net/ – Maciej Kwas Nov 19 '15 at 10:50