I have a class hierarchy that represents the records in a database, one class for each table. I want to be able to properly type check the class objects (that represent the tables) as well as the instances of the class (that represent the individual rows in the database). The only way I can get my code to compile is to disable type checking by casting the variables that hold the class to <any>
. Below is a contrived example that demonstrates the issue/questions I am having.
My primary question is about types for variables holding classes. The answer to all three of these questions should, I think, have the same answer:
name
dictionary (defined on line 3 of the code)?cls
(defined on line 31)?cls
be in the function maybeMakeInstance
on line 49Bonus questions:
names
defined on line 3) is never defined in a subclass?tablename
and all
defined on lines 6 and 7 and redefined on lines 61 and 62)?class A {
// this should not be inheritable
static names:{ [name: string]: { new(key:string):A} } = {};
// next two need to be inherited in each subclass
static tablename:string = "Aclass";
static all: Record<string, A> = {};
id: string;
// create a new object and put into per class cache, `all`
constructor(key:string = null) {
this.id = key;
if (key != null) {
new.target.all[key] = this;
}
}
// return instance matching `key`. Will only search in exact class (not subclasses or superclasses)
static find(key:string):A|null {
if (key in this.all) {
return this.all[key];
}
console.log(`${key} not in ${this.tablename}`);
return null;
}
// pretty print info about instance
show():void {
// What is proper type for `this.constructor`? <{ new(key:string):A}> fails as does doing nothing.
const cls = <any>this.constructor;
console.log(`${this.id} from ${cls.tablename}`);
}
// pretty print info about instance
static showall():void {
for (let x in this.all) {
this.all[x].show();
}
}
static init(name:string):void {
this.names[name] = this;
}
static maybeMakeInstance(clsname:string, key:string):A {
if ( !(clsname in A.names) ) throw new Error(`unknown classname: ${clsname}`);
// what is proper type of `cls`?
let cls:any = A.names[clsname];
if (key in cls.all) {
console.log(`Reusing ${key} in class ${clsname}/${cls.tablename}`);
return cls.all[key];
}
return new cls(key);
}
};
A.init('classA');
class B extends A {
// is this proper way to override superclass static members?
static tablename:string = "Bclass";
static all: Record<string, B> = {};
}
B.init('classB');
// make sure above code is working.
function main() {
let a = new A('first');
A.showall();
A.find('first').show();
new A('second');
new B('one');
A.showall();
B.showall();
console.log(B.find('first'));
console.log(B.find('second'));
console.log(B.find('one'));
console.log(A.find('one'));
A.maybeMakeInstance('classA', 'third');
A.maybeMakeInstance('classB', 'two');
A.maybeMakeInstance('classB', 'two');
console.log('------ A');
A.showall();
console.log('------ B');
B.showall();
A.maybeMakeInstance('classA', 'two');
console.log('------ A');
A.showall();
}
main();
////////////////
// running this file will result in the following output:
////////////////
// first from Aclass
// first from Aclass
// first from Aclass
// second from Aclass
// one from Bclass
// first not in Bclass
// null
// second not in Bclass
// null
// B { id: 'one' }
// one not in Aclass
// null
// Reusing two in class classB/Bclass
// ------ A
// first from Aclass
// second from Aclass
// third from Aclass
// ------ B
// one from Bclass
// two from Bclass
// ------ A
// first from Aclass
// second from Aclass
// third from Aclass
// two from Aclass
////////////////
The answer to both bonus questions is basically "you can't" which to me indicates that this isn't a great design. So I basically rewrote your code rather than answering your typing questions, which I hope is helpful.
Your classA
is trying to do too much. It represents an individual table object, stores all instances of its own type, and stores the names of all other types. Let's break those three uses into their own classes.
Each object instance is a TableObject
. Its only method it print itself.
class TableObject {
id: string;
table: Table;
// TableObject stores a reference to the table that it is in
constructor (table: Table, id: string) {
this.id = id;
this.table = table;
}
// pretty print info about instance
show():void {
console.log(`${this.id} from ${this.table.tablename}`);
}
}
Each class name is a Table
. The table stores the object instances, can find or create objects, and can print all objects.
class Table {
tablename: string;
all: Record<string, TableObject> = {};
// construct a table from a name
constructor( tablename: string ) {
this.tablename = tablename;
}
// return TableObject instance matching `key`.
find(key:string): TableObject | null {
const obj = this.all[key];
if ( obj ) return obj;
console.log(`${key} not in ${this.tablename}`);
return null;
}
// create a new TableObject instance and put into per class cache, `all`
create(key:string): TableObject {
const obj = new TableObject(this, key);
this.all[key] = obj;
return obj;
}
// pretty print info about all TableObject instances in this table
showAll(): void {
for (let x in this.all) {
this.all[x].show();
}
}
}
The different tables are stored in a Database
. The database can execute the maybeMakeInstance
and showAll
functions from your previous class A
.
class Database {
// store an object of Table classes keked by table name
tables: Record<string, Table> = {};
// can create an empty Database, or initialize with some Tables
constructor( tables: Table[] = [] ) {
tables.forEach(t => this.addTable(t));
}
// add an existing Table object to the Database
addTable( table: Table ): void {
this.tables[table.tablename] = table;
}
// create a new Table object and add it to the Database
createTable( tablename: string ): Table {
const table = new Table(tablename);
this.addTable(table);
return table;
}
// retrieve Table object by name
getTable(tablename: string): Table | null {
return this.tables[tablename] ?? null;
}
// find or create a TableObject based on the table name and object key
maybeMakeInstance(tablename:string, key:string): TableObject {
const table = this.getTable(tablename);
if ( ! table ) throw new Error(`unknown table name: ${tablename}`);
const existing = table.find(key);
if (existing) {
console.log(`Reusing ${key} in table ${tablename}`);
return existing;
}
return table.create(key);
}
// show all result from all tables
showAll(): void {
Object.values(this.tables).forEach(t => t.showAll());
}
}
Your code sample becomes something like this:
function main() {
const tableA = new Table('classA');
tableA.create('first');
tableA.showAll();
tableA.find('first')?.show();
tableA.create('second');
const database = new Database([tableA]);
const tableB = database.createTable('classB')
tableB.create('one');
tableA.showAll();
tableB.showAll();
console.log(tableB.find('first'));
console.log(tableB.find('second'));
console.log(tableB.find('one'));
console.log(tableA.find('one'));
database.maybeMakeInstance('classA', 'third');
database.maybeMakeInstance('classB', 'two');
database.maybeMakeInstance('classB', 'two');
console.log('------ A');
tableA.showAll();
console.log('------ B');
tableB.showAll();
database.maybeMakeInstance('classA', 'two');
console.log('------ A');
database.showAll();
}
main();
It is now able to distinguish between different tables, with the final output printing:
"first from classA"
"second from classA"
"third from classA"
"two from classA"
"one from classB"
"two from classB"
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.