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.