10

Is it possible to write a Mapped Type that transforms a Class into an Interface minus the class's methods or any properties assigned to the prototype. EG,

class Dog {
    name: string;
    age: number;
    bark() {}
    get dogYears(): number {
        return this.age * 7;
    }
}

type InterfaceOf<T> = { /* ??? */ };
type IDog = InterfaceOf<Dog>;
// equivalent to
interface IDog {
    name: string;
    age: number;
}

Why do I want to do this? I'm looking to "deserialize" json objects into classes. Eg, I run a query to get dogs from the database, afterwards I'd like to instantiate them into class objects, perhaps by using the class-transformer library.

function queryDogs(): Promise<{ name: string, age: string }[]>;
function deserialize<T>(cls: (new() => T), input: InterfaceOf<T>): T;
function deserialize<T>(cls: (new() => T), inputs: InterfaceOf<T>[]): T[];

class Dog {
    @Type(() => String)
    name: string;
    @Type(() => Number)
    age: number;
    bark() {}
    get dogYears(): number {
        return this.age * 7;
    }
}

const dbDogs = await queryDogs();
const dogs: Dog[] = deserialize(Dog, dogs);

It would be nice if the deserialize function knew if the input was the right shape to be deserialized into the Dog class. I was hoping it could look at the Dog class that is given to it to transform it into the appropriate interface.

sparebytes
  • 12,546
  • 3
  • 21
  • 32

2 Answers2

15

Interface Based on Class

You can directly generate an interface from a class in TypeScript:

interface DogLike extends Dog {

}

The Angular community is all over this, but beware of some warnings about using classes as interfaces.

The interface that this would generate for you would include properties and methods:

interface DogLike {
    name: string;
    age: number;
    bark(): void;
    dogYears: number;
}

Mapped Type Madness

Now, you can do something really clever/complex/mad with mapped types, based on the mapped types found in this article.

type Remove<T extends string, U extends string> = ({[P in T]: P } & {[P in U]: never } & { [x: string]: never })[T];
type RemoveProperty<T, K extends keyof T> = { [P in Remove<keyof T, K>]: T[P] };

type PartDog = RemoveProperty<DogLike, 'bark' | 'dogYears'>;

This takes the DogLike interface, and removes bark and dogYears.

I included this as you mentioned mapped types. However, it is an insane solution.

Interface

My recommended solution, would be a a simple interface, and perhaps one that isn't named after dogs at all, as the properties are more general:

interface NamedLivingOrganism {
    name: string;
    age: number;
}

Okay, you may not name it exactly like that. However, simple is often best and when you come to change either the Dog class, or the interface that is loosely based on it at some point in the future, the simple interface will prove to be the best way to represent what you need.

Fenton
  • 241,084
  • 71
  • 387
  • 401
  • 1
    Thanks for the detailed answer and the warning. I updated my question with an example of how I could use it. I feel like my use-case doesn't quite fall into that warning scenario. Perhaps using Typescript decorators in this way should come with similar warnings? – sparebytes Dec 20 '17 at 20:32
  • I'd also like to suggest that there is also the `Pick` approach which is almost as painful as the the `Remove` approach. Another problem with the `Pick` and `Remove` approaches is they break down for nested classes, eg if the `Dog` class had an `owner` property of `Human` – sparebytes Dec 20 '17 at 20:35
  • 1
    After seeing the madness of Mapped Type Madness, I realised the Interface solution was the simplest for my case, so thank you for showing me the bottom of the rabbit hole =D – Henry Blyth Dec 11 '19 at 19:08
15

It's possible to define the IDog type without methods, as described in TS documentation. But unfortunately it still contains dogYears getter. As I know, it cannot be solved because when dealing with types, there is no difference between fields and getters.

type NonFunctionPropertyNames<T> = {
    [K in keyof T]: T[K] extends Function ? never : K
}[keyof T];
type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;

type IDog = NonFunctionProperties<Dog>;
Alexey Prokhorov
  • 3,431
  • 1
  • 22
  • 25
  • The secret is in `{...}[keyof T]` syntax, described [here](https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html). It compiles the type as a union of the properties except if its types wasn't `"never"`. So, for type `type AAnyType = { foo: "foo", bar: "bar", fn1: never };` (mapped by `{ [K in keyof T]: T[K] extends Function ? never : K }`), the type `type NoNeverUnion = AAnyType["foo" | "bar" | "fn1"];` maps `"foo" | "bar"`, and `"foo" | "bar" | "fn1"` is maped by `keyof AAnyType`, so `AAnyType[keyof AAnyType];` has the same effect. Very well. Thank you. – rplaurindo Feb 02 '22 at 00:43
  • This is so elegant, thank you. I used it to make a sort of factory with named arguments, without having to type everything twice. https://stackoverflow.com/a/72478300/3917091 – Regular Jo Jun 02 '22 at 15:01
  • Is there a ready lib that provides this type? – Maciej Sz Feb 09 '23 at 23:56