Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript dynamically create interface

Tags:

I use simple-schema to define DB schemas in an object:

{    name: 'string',    age: 'integer',    ... } 

Is it somehow possible to create an interface or class from this object, so I don't have to type everything twice?

like image 741
Chris Avatar asked Aug 19 '17 12:08

Chris


2 Answers

You can do this, but it might be more trouble than it's worth unless you think you might be changing the schema. TypeScript doesn't have built-in ways of inferring types in a way that you want, so you have to coax and cajole it to do so:


First, define a way of mapping the literal names 'string' and 'integer' to the TypeScript types they represent (presumably string and number respectively):

type MapSchemaTypes = {   string: string;   integer: number;   // others? }  type MapSchema<T extends Record<string, keyof MapSchemaTypes>> = {   -readonly [K in keyof T]: MapSchemaTypes[T[K]] } 

Now if you can take an appropriately typed schema object like the one you specified, and get the associated type from it:

const personSchema = {name: 'string', age: 'integer'};  type Person = MapSchema<typeof personSchema>; // ERROR 

Oops, the problem is that personSchema is being inferred as {name: string; age: string} instead of the desired {name: 'string'; age: 'integer'}. You can fix that with a type annotation:

const personSchema: { name: 'string', age: 'integer' } = { name: 'string', age: 'integer' };  type Person = MapSchema<typeof personSchema>; // {name: string; age: number}; 

But now it feels like you're repeating yourself. Luckily there is a way to force it to infer the proper type:

function asSchema<T extends Record<string, keyof MapSchemaTypes>>(t: T): T {   return t; } const personSchema = asSchema({ name: 'string', age: 'integer' }); // right type now type Person = MapSchema<typeof personSchema>; // {name: string; age: number}; 

UPDATE 2020-06: in more recent TS versions you can use a const assertion to get the same result:

const personSchema = { name: 'string', age: 'integer' } as const; type Person = MapSchema<typeof personSchema>; 

That works!


See it in action on the Typescript Playground. Hope that helps; good luck!

like image 124
jcalz Avatar answered Sep 22 '22 14:09

jcalz


I don't think you can declare dynamic interfaces. However, you can create a type for objects with known properties.

You can create an object that maps string literals to actual types, e.g. 'integer' => number, but that is not relevant to the question. I don't know what framework you're using but the following example works for a similar looking framework: Mongoose.

users.js

export const UserSchema = mongoose.Schema({     name: String,     value: Number });  export const Users = mongoose.Model('users', UserSchema);  export type User = { [K in keyof typeof UserSchema]: any } ; 

usage:

import { User, Users } from './user';  Users.find({}).exec((err: Error, res: User) => { ... }) 

The returned result should have the same keys as UserSchema, but all values are mapped to any as you would still have to map string literals to types.

like image 34
Shane Avatar answered Sep 21 '22 14:09

Shane