This is a demo project to illustrate the usage of io-interface.
-
npm install -D ts-patch -
add "postinstall" script to
package.jsonto auto-patch the compiler afternpm install{ "scripts": { "postinstall": "ts-patch install" } } -
npm install -D io-interface -
add transformer to
tsconfig.json{ "compilerOptions": { "plugins": [{ "transform": "io-interface/transform-interface" }] } }
To verify the setup, add this testing code to app.component.ts
import { schema } from 'io-interface';
interface Order {
price: number;
date: Date;
note?: string;
pricelines: number[];
}
const OrderSchema = schema<Order>();
console.log(OrderSchema);You should see the console message like this:
The example code is as follows. src/app/services/decoder.service.ts
import { Injectable } from '@angular/core';
import { Decoder, schema } from 'io-interface';
import { BadTodo } from '../models/bad-todo';
import { Todo } from '../models/todo';
@Injectable({
providedIn: 'root',
})
export class DecoderService {
readonly schemas = [schema<Todo>(), schema<BadTodo>()];
readonly dec = new Decoder(this.schemas);
decode<T>(typeName: string, data: unknown): T | undefined {
return this.dec.decode<T>(typeName, data, console.error);
}
decodeArray<T>(typeName: string, data: unknown): T[] | undefined {
return this.dec.decodeArray<T>(typeName, data, console.error);
}
}Example: src/app/models/todo.ts.
// src/app/models/todo.ts
export interface Todo {
userId: number;
id: number;
title: string;
completed: boolean;
}// src/app/services/decoder.service.ts
readonly schemas = [schema<Todo>()];src/app/services/todo.service.ts
// src/app/services/todo.service.ts
getTodo(): Observable<Todo> {
return this.http.get('https://jsonplaceholder.typicode.com/todos/1').pipe(
map(json => this.dec.decode<Todo>('Todo', json)),
filter(todo => !!todo),
);
}And that's it! Now in todo.component.ts we can use the data as Todo without worrying about the data is in a wrong layout/value.
// src/app/todo/todo.component.ts
todo: Todo;
this.service.getTodo().subscribe(todo => (this.todo = todo));As you can see from the signature decode<Todo>('Todo', json), Todo repeats twice. But for native TypeScript this is needed because the type parameter is for static environment and method parameter is for runtime environment. I don't find a very good solution here but I created a specific TypeScript transformer to expand a macro such as decode<Todo>(json) to decode<Todo>('Todo', json). The solution will not shared here but you get the idea. Since TypeScript will never populate the interface information to runtime so I guess this would be the easiest way to reduce the duplication.
In the example DecoderService, class Decoder provides a errors method to generate a string[] from an invalid decoding result. Say if we somehow made the interface definition out of sync with the backend:
export interface BadTodo {
user_id: number;
id: string;
title: string;
description: string;
completed: boolean;
}And if you try to decode to this interface from the same backend data. You will see this error poped up.

