Check if an object implements an interface at runtime with TypeScript

JavascriptTypescript

Javascript Problem Overview


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?

Javascript Solutions


Solution 1 - Javascript

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

Solution 2 - Javascript

> 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

Solution 3 - Javascript

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.

Solution 4 - Javascript

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

Solution 5 - Javascript

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

Solution 6 - Javascript

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

Solution 7 - Javascript

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

Solution 8 - Javascript

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.

Solution 9 - Javascript

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

Solution 10 - Javascript

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.

Solution 11 - Javascript

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
}

Solution 12 - Javascript

Just made simple site for generating JavaScript validation code out of typescript interfaces. Note: Read the limitation carefully.

https://ts-interface-validator.vercel.app/

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionMasterScratView Question on Stackoverflow
Solution 1 - JavascriptTeodor SanduView Answer on Stackoverflow
Solution 2 - JavascriptMasterScratView Answer on Stackoverflow
Solution 3 - JavascriptDS.View Answer on Stackoverflow
Solution 4 - JavascriptDS.View Answer on Stackoverflow
Solution 5 - JavascriptJthorpeView Answer on Stackoverflow
Solution 6 - JavascriptpcanView Answer on Stackoverflow
Solution 7 - JavascriptTrident D'GaoView Answer on Stackoverflow
Solution 8 - JavascriptMoshe GottliebView Answer on Stackoverflow
Solution 9 - JavascriptMarkus JohnssonView Answer on Stackoverflow
Solution 10 - JavascriptMaciej KwasView Answer on Stackoverflow
Solution 11 - JavascriptAlexey BaranoshnikovView Answer on Stackoverflow
Solution 12 - JavascriptShreyas JadhavView Answer on Stackoverflow