1

I'm working on a pattern where an abstract parent class needs to inherit all the properties of the generic type supplied through its subclasses.

Here's a look at the constructor for the base class which gives a basic idea, and can also be viewed on github:

public constructor(data: T) {

    _.assign(this, _.omit(data, [
        "table",
        "id",
        "PartitionKey",
        "RowKey",
    ]));
}

What I'm looking for is some way to effectively express this:

export abstract class Base<T extends EntityData> implements Storage.AbstractEntity & T {
...

Unfortunately, this doesn't appear to be supported currently and so I'm at a loss as to how I can make my base class extend its generic parameter such that any class that inherits from it also receives the properties of the generic type it's encapsulating:

export class AuthorFact extends Base<Author> {

    public get RowKey() { return (this as any).id }
}

As you can see, I'm forced to remove & T from my base type and use (this as any) to suppress the compiler throwing an error.

What I'd like ultimately is for the type check on .id to succeed, as well as any other properties of Author to be available on the instance I'mm creating.

1 Answer 1

3

Even with the latest commit that allows classes and interfaces to derive from object types and intersections of object types, extending classes from generic type parameter is still not allowed:

An interface or class cannot extend a naked type parameter because it is not possible to consistently verify there are no member name conflicts in instantiations of the type.

But what is now allowed is crazy: you can build classes at run time and have them type-checked. (this requires npm i typescript@next which currently is 2.2):

import * as Azure from "azure";
import * as _ from "lodash";


export class Author {
    public id: string;
    public firstName: string;
    public lastName: string;
    public nativeIds: {[key: string]: string} = {};
    public posts: Post[];
    public created: Date;
}

export class Post {
   public constructor(
        public id: string,
        public author: Author,
        public title: string,
        public content: string,
        public authored: Date
    ) {
    }
}

type Constructor<T> = new () => T;
type DataConstructor<T, Data> = new (data: Data) => T;

function Base<T>(dataClass: Constructor<T>) {
    class Class extends (dataClass as Constructor<{}>) {
        public constructor(data: T) {
            super();
            _.assign(this, _.omit(data, [
                "table",
                "id",
                "PartitionKey",
                "RowKey",
            ]));
        }
        [property: string]: string | number | boolean | Date;
    }
    return Class as Constructor<T & Class>;
}

function Fact<T, Data>(superClass: DataConstructor<T, Data>) {
    class Class extends (superClass as DataConstructor<{}, Data>) {
        public get PartitionKey() { return "fact" }
    }
    return Class as DataConstructor<T & Class, Data>
}

function Identifiable<T, Data>(superClass: DataConstructor<T, Data>) {
    class Class extends (superClass as DataConstructor<{}, Data>) {
        public id: string;
        public get RowKey() { return this.id }
    }
    return Class as DataConstructor<T & Class, Data>
}

function IdentifiableDataFact<Data>(dataClass: Constructor<Data>) {
    return Identifiable(Fact(Base(dataClass)));
}


class AuthorFact extends IdentifiableDataFact(Author) {
}

// let's init some data
let author = new Author();
author.id = 'a';
author.firstName = 'z';
author.lastName = 'q';


// let's see what we've got    
let authorFact = new AuthorFact(author); // it has a constructor that takes Author

let e: Azure.Entity = authorFact; // it's structurally compatible with Azure.Entity

// it has PartitionKey
console.log(authorFact.PartitionKey);         // prints fact

// it has some properties that were copied over by Base constructor (except id)
console.log(authorFact.firstName);     // prints z

// it has index signature (check with --noImplicitAny)
const ps = ['lastName', 'nativeIds'];
ps.forEach(p => console.log(authorFact[p]));  // prints q {}

// and it has RowKey but it's undefined here (this might not be what you want)
// because id is explicitly omitted from being copied in Base constructor
console.log(authorFact.RowKey);    // undefined 

It turns out that you can't do that with abstract classes, but I think structural types still allow you to do what you want here.

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

3 Comments

Is there any chance I might be able to use mapped types? Also, why are you using functions instead of classes to wrap some things?
No idea about mapped types. Functions are necessary because you can't do type T = A & B; class C extends T {} because for extends, T has to be a value as well as a type - you get error TS2693: 'T' only refers to a type, but is being used as a value here. Function that returns class type is the only way to do that.
I'm going to keep your answer in mind as it seems like one workable approach. I just want to verify that newer language features haven't landed that might yield a less...circuitous...solution ;)

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.