38

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?

3 Answers 3

61

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!

Sign up to request clarification or add additional context in comments.

5 Comments

Is there a way to make it work like this? const personObj = { name: 'string', age: 'integer' } type Person = MapSchema<typeof asSchema(personObj)>; I have been very unsuccessful at encapsulating this.
Can this answer be improved using the new Typescript 4.1? devblogs.microsoft.com/typescript/announcing-typescript-4-1 I am unsure, still unfamiliar with advanced TS features.
@nstrelow I don't see how; the current version is pretty minimal. If we were remapping keys as well as values then the new as-clauses-in-mapped-types would help but we're not.
That is cool. Now, if it was recursive on a deep opbject... =)
This works because the keys are all different types, but if you have string, string, from what I've tested, it doesnt work
3

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.

2 Comments

Is it possible to assert that a type (User) contains every key of the javascript object (UserSchema)?
I wanted to do some something similar to @jcalz MapSchema util, but I'm not working with any sort of real database schema, I just have a simple object of field names and their default values. I didn't like using the { [K in keyof Thing]: string } syntax cause I wanted to assert that every key of my "schema" is enforced for the created type - I was able to answer my own question here: stackoverflow.com/questions/64778252/…
0

Here is an example if you have defined the type in your object e.g. when you use mongoose schema

const schema = {
    name: { 
        type: String,
        required: true,
        unique: true,
        index: true
    },
    decimals: { type: BigInt, required: true }
}

type MongooseSchemaInterface = {
    [K in keyof typeof schema]: MongooseSchemaType<typeof schema[K]['type']>;
};

const test: MongooseSchemaInterface = {
    name: 123 // <-- Will not work
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.