👨💻 Web Developer
☮️ Open Source Enthusiast
✍️ Writer
👨🍳 Foodie/Cook
🌎 Traveler
My Application is fully typed, it's 100% bug-free
If it builds, it works in production
We're using typescript, there is no need to test
// Typescript definition
type ExampleType = {
name: string;
age?: number;
pets: {
name: string;
legs: number;
}[];
};
// communicates with external API
const fetchData = (): Promise<ExampleType> => {};
const getBiped = async () => {
const data = await fetchData();
// { name: 'John' }
return data.pets.find((pet) => pet.legs === 2); // Boom
};
Validate all the external sources of your Application.
const validate = (data: ExampleType) => {
if (!data.pets) return false;
// perform more checks
return true;
};
const getBiped = async () => {
const data = await fetchData();
if (!validate(data)) {
throw Error('Validation Error: structure not valid ...');
}
return data.pets.find((pet) => pet.legs === 2);
};
✅ No external library required
✅ Business focused
❌ Manual work
❌ Duplication of code
ajv joi v8n validate.js
import Ajv from 'ajv'
const ajv = new Ajv();
const validate = ajv.compile({
properties: {
name: {
type: 'string',
minLength: 3,
},
age: { type: 'number' },
pets: {
type: 'array',
items: {
name: {
type: 'string',
minLength: 3,
},
legs: { type: 'number' },
},
},
},
required: ['name', 'pets'],
type: 'object',
});
const getBiped = async () => {
const data = await fetchData();
if (!validate(data)) {
throw Error('Validation failed: ' + ajv.errorsText(validate.errors));
// Error: Validation failed: data should have required property 'pets'
}
return data.pets.find((pet) => pet.legs === 2);
};
✅ A standardized way to create validators and checks
✅ Improvement of Error Reporting
❌ Introduction of new Syntax
❌ Validators and Types are not in Sync*
Automatically generate a validator using JSON Schema and AJV for any TypeScript type.
Made by @ForbesLindesay 👏
// src/types/ExampleType.ts
type ExampleType = {
name: string;
age?: number;
pets: {
name: string;
legs: number;
}[];
};
> npx typescript-json-validator src/types/ExampleType.ts ExampleType
# ExampleType.validator.ts created!
/* tslint:disable */
// generated by typescript-json-validator
import { inspect } from "util";
import Ajv = require("ajv");
import ExampleType from "./ExampleType";
// Instance of Ajv
export const ajv = new Ajv({
allErrors: true,
coerceTypes: false,
format: "fast",
nullable: true,
unicode: true,
uniqueItems: true,
useDefaults: true,
});
ajv.addMetaSchema(require("ajv/lib/refs/json-schema-draft-06.json"));
export { ExampleType };
// Definition of Schema
export const ExampleTypeSchema = {
$schema: "http://json-schema.org/draft-07/schema#",
defaultProperties: [],
properties: {
age: {
type: "number",
},
name: {
type: "string",
},
pets: {
items: {
defaultProperties: [],
properties: {
legs: {
type: "number",
},
name: {
type: "string",
},
},
required: ["legs", "name"],
type: "object",
},
type: "array",
},
},
required: ["name", "pets"],
type: "object",
};
export type ValidateFunction<T> = ((data: unknown) => data is T) &
Pick<Ajv.ValidateFunction, "errors">;
export const isExampleType = ajv.compile(ExampleTypeSchema) as ValidateFunction<
ExampleType
>;
// Expose validate function
export default function validate(value: unknown): ExampleType {
if (isExampleType(value)) {
return value;
} else {
throw new Error(
ajv.errorsText(
isExampleType.errors!.filter((e: any) => e.keyword !== "if"),
{ dataVar: "ExampleType" }
) +
"\n\n" +
inspect(value)
);
}
}
import validate from 'src/types/ExampleType.validator';
const getBiped = async () => {
const data = validate(await fetchData());
return data.pets.find((pet) => pet.legs === 2);
};
interface ExampleType {
/**
* @format email
*/
email?: string;
/**
* @minimum 0
* @maximum 100
*/
answer: number;
}
export const ExampleTypeSchema = {
$schema: 'http://json-schema.org/draft-07/schema#',
defaultProperties: [],
properties: {
answer: {
maximum: 100,
minimum: 0,
type: 'number',
},
email: {
format: 'email',
type: 'string',
},
},
required: ['answer'],
type: 'object',
};
pre commands 🎉
{
"scripts": {
"prestart": "ts-node generate-validators.ts src/types",
"start": "react-scripts start",
"prebuild": "ts-node generate-validators.ts src/types",
"build": "react-scripts build",
}
}
➜ my-project git:(master) ✗ yarn start
yarn run v1.22.5
$ ts-node generate-validators.ts src/types
Generating validators from src/types ...
- AnotherType.validator.ts created!
- ExampleType.validator.ts created!
$ react-scripts start
import { readdirSync, statSync } from 'fs';
import { promisify } from 'util';
const exec = promisify(require('child_process').exec);
const VALIDATOR_EXTENSION = '.validator.ts';
const [dir] = process.argv.slice(2);
if (!statSync(dir).isDirectory)
throw new Error('Please specify a directory ...');
console.log(`Generating validators from ${dir} ...`);
const types = readdirSync(dir)
.filter((x) => !x.endsWith(VALIDATOR_EXTENSION))
.map((type) => type.split('.')[0]);
Promise.all(
types.map(async (type) => {
const command = `yarn typescript-json-validator ${dir}/${type}.ts ${type}`;
const { stderr } = await exec(command);
if (stderr)
throw new Error(
`There was an error while creating the validators: ${stderr}`,
);
console.log(`- ${type}${VALIDATOR_EXTENSION} created!`);
}),
);
import { statSync } from 'fs';
import { watch } from 'chokidar';
import { basename } from 'path';
import { promisify } from 'util';
const exec = promisify(require('child_process').exec);
const readFile = promisify(require('fs').readFile);
const writeFile = promisify(require('fs').writeFile);
const VALIDATOR_EXTENSION = '.validator.ts';
const [dir] = process.argv.slice(2);
if (!statSync(dir).isDirectory)
throw new Error('Please specify a directory ...');
console.log(`Generating validators from ${dir} ...`);
const generateValidator = async (path: string) => {
const { stderr } = await exec(
`yarn typescript-json-validator ${path} ${basename(path, '.ts')}`,
);
if (stderr)
throw new Error(
`There was an error while creating the validators: ${stderr}`,
);
console.log(`- ${path.replace('.ts', VALIDATOR_EXTENSION)} created!`);
};
watch(dir, {
ignored: `**/*${VALIDATOR_EXTENSION}`,
})
.on('add', generateValidator)
.on('change', generateValidator);
Dashboard to provide a quick overview of Open Source projects in Github
Get Gist Information
Fetch Projects Information
Compare Threshold against Projects
// Settings.ts
export type Threshold = {
pullRequests?: number;
issues?: number;
};
export type Settings = {
projects: string[];
threshold?: Threshold;
};
export default Settings;
// Project.ts
export type Stargazer = {
id: string;
name: string | null;
avatarUrl: string;
};
export type Countable = {
totalCount: number;
};
export type Project = {
id: string;
url: string;
name: string;
pullRequests: Countable;
vulnerabilityAlerts: Countable;
issues: Countable;
stargazers: Countable & {
nodes: Stargazer[];
};
};
export default Project;
import { isQueryReady } from '../utils/queries';
import { useQuery } from '@apollo/react-hooks';
import { QueryResult } from '@apollo/react-common';
import { ApolloError } from 'apollo-boost';
import { EMPTY_SETTINGS } from '../utils/constant';
import { INVALID_SETTINGS_ERROR } from '../utils/error';
import { Settings } from '../types/Settings';
import { Query } from '../queries/ProjectQuery';
import validateSettings from '../types/Settings.validator';
type SettingsQueryResult = QueryResult<QueryData> & { output: Settings };
const useSettingsQuery = (gistName: string): SettingsQueryResult => {
const projectsQuery = useQuery(Query, {
variables: { name: gistName },
skip: !gistName,
});
if (isQueryReady(projectsQuery)) {
const { viewer } = projectsQuery.data!;
const gistFile = viewer.gist.files[0];
try {
const gistContent = JSON.parse(gistFile.text);
const settings = validateSettings(gistContent);
return {
...projectsQuery,
output: settings,
};
} catch (error) {
return {
...projectsQuery,
output: EMPTY_SETTINGS,
error: new ApolloError({
errorMessage: INVALID_SETTINGS_ERROR,
extraInfo: error,
}),
};
}
}
return {
...projectsQuery,
output: EMPTY_SETTINGS,
};
};
export default useSettingsQuery;
import { isQueryReady } from '../utils/queries';
import { useQuery } from '@apollo/react-hooks';
import { Query, QueryData } from '../queries/RepositoriesQuery';
import { QueryResult } from '@apollo/react-common';
import Project from '../types/Project';
import validateProject from '../types/Project.validator';
import { projectNameToParts } from '../utils/string';
type ProjectDataQueryResult = QueryResult<QueryData> & { output: Project[] };
const DEFAULT: string[] = [];
const useProjectDataQuery = (projects = DEFAULT): ProjectDataQueryResult => {
const repositoriesQuery = useQuery<QueryData>(Query(projects), {
skip: projects.length === 0,
});
if (isQueryReady(repositoriesQuery)) {
try {
const projectsWithData = projects
.map((projectName) => {
const { key } = projectNameToParts(projectName);
return repositoriesQuery.data![key];
})
.filter(Boolean)
.map(validateProject);
return {
...repositoriesQuery,
output: projectsWithData,
};
} catch (error) {
return {
...repositoriesQuery,
error: new ApolloError({
errorMessage: INVALID_PROJECTS_ERROR,
extraInfo: error,
}),
output: [],
};
}
}
return {
...repositoriesQuery,
output: [],
};
};
export default useProjectDataQuery;
Approach
Manual
Library
Dynamic Types
No New Syntax
✅
❌
✅
Validators and Types in Sync
❌
❓
✅
Standardization
❌
✅
✅