Oftentimes when designing with typescript, it's desirable to also do some extra type checking at runtime if:
When writing typescript I often find myself repeating type information in multiple paradigms if I'm working, for example, with Models in sequelize or if I want to include PropTypes in a React project (so that downstream JavaScript users can be typesafe).
If there was an irreversible operation for producing a standardized schema object from a type at compile time, common solutions could be constructed for both of these problems.
I'm imagining something like
type user = {id: number, userName: string, password: string};
const userSchema: TypeSchema<user> = &user;
// Integrate with other type systems
const sequelizeModelFromTypeSchema = <T>(schema: TypeSchema<T>): Sequelize.Model<Instance<T>, T> => {
...
};
let userModel = sequelizeModelFromTypeSchema(userSchema);
// Run time type checking
collectionWhichContainsSomeUsersAndOtherStuff.forEach((maybeUser: any) => {
if (thirdPartyTypeCheck(userSchema, maybeUser)) {
let definitelyUser: user = maybeUser;
...
}
};
...
Which would be compiled to:
const _schema_user = {
id: {type: 'number', required: true},
userName: {type: 'string', required: true},
password: {type: 'string', required: true}
}
const userSchema = _schema_user;
const sequelizeModelFromTypeSchema = (schema) => { ... };
let userModel = sequelizeModelFromTypeSchema(userSchema);
collectionWhichContainsSomeUsersAndOtherStuff.forEach((maybeUser) => {
if (thirdPartyTypeCheck(userSchema, maybeUser)) {
let definitelyUser = maybeUser;
...
}
};
...
Any time that a type contains a reference to another type, schemas for the parent and child would need to be created. They would be given some predictable, prefixed name (such as _schema_{typeName} used above) at the top of the compiled JS file.
i.e. with user defined as above...
type account = {id:number, owner: user};
console.log(&account);
is compiled to
const _schema_user = {
id: {type: 'number', required: true},
userName: {type: 'string', required: true},
password: {type: 'string', required: true}
};
const _schema_account = {
id: {type: 'number', required: true}
owner: {type: _schema_user, required: true}
};
console.log(_schema_account);
Obviously, this is a one-way operation. No setting the schema, i.e. &user = {id: 'string'}
It's hard to say how this operation would best handle recursively defined types or generics. I would think the easiest approach would be to restrict to a very specific subcategory of easy-to-schemify types and throw compile-time errors if users try to schemify something ridiculous. Even if its use was restricted, this operation would still handle the most common and useful cases gracefully. Besides, if someone has a generic or recursive, they should probably specify it by hand in both type systems and write custom typecheckers.
The other approach would be to expose as much information from the type-system as possible and allow downstream library developers to use that information creatively.
I think it's clear that passing type information into a runtime container would lead to useful applications. I don't think this interferes with any of typescripts design goals; all extra generated code will be explicitly requested, inserting the type definition at the top of a file would be a constant time operation, developers would only be using this under circumstances where they need to write the same code by hand otherwise.
I think we could expect to expand developers capacity to express intention to downstream JavaScript users. Such an operator would likely facilitate the development of libraries for DRYer typescript development on top of JS frameworks which have their own type systems.
@pixelpax You may want to check out https://github.com/gcanti/io-ts.
Another option is https://github.com/pelotom/runtypes
Providing a runtime type system is not withing the scope of the TS project. it is, and has been, one of our explicit non-goals.
Most helpful comment
Another option is https://github.com/pelotom/runtypes