简体   繁体   中英

How to properly type a variable that is a class in typescript

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:

  • What type should I give to the values in the name dictionary (defined on line 3 of the code)?
  • What type should I give to cls (defined on line 31)?
  • Similarly, what type should cls be in the function maybeMakeInstance on line 49

Bonus questions:

  • Is there a way to ensure that a static member (eg, names defined on line 3) is never defined in a subclass?
  • What is the proper way to declare static members that MUST be redefined in subclasses (eg, 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" 

Typescript Playground Link

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM