After playing around with this for a while, I came across another solution that fits my needs near perfectly:
// --- Properties interface to set up getObj/setObj --- //
interface _Properties<T> {
getObj<U extends keyof T>(name: U): T[U]
setObj<U extends keyof T>(name: U, value: T[U]): void;
setObj<U extends keyof T>(map: { [key in U]: T[key] }): void;
}
// --- Some data class interfaces that the TextBox classes use --- //
interface SomeValue {
a: number;
}
interface MoreSomeValue extends SomeValue {
b: string;
}
// --- Define properties for the classes --- //
interface TextBoxProps<ValueType = string> {
value: ValueType; // Completely changing type of prop in inherited class can (must?) be done via generics
disabled: boolean;
test: SomeValue; // But overriding to return more specific is OK
}
interface ValidationTextBoxProps extends TextBoxProps<number> {
min: number;
max: number;
test: MoreSomeValue
}
// --- Actual class interfaces extending properties --- //
interface TextBox extends _Properties<TextBoxProps> { }
interface ValidationTextBox extends _Properties<ValidationTextBoxProps>, TextBox { }
// --- Constructor that allows properties to be set --- //
interface ValidationTextBoxConstructor {
// Partial since all attributes are optional
new(props: Partial<ValidationTextBoxProps>): ValidationTextBox;
}
declare const ValidationTextBoxConstructor: ValidationTextBoxConstructor; // Actual constructor
// --- Usage --- //
// The following is all type checked at compile time, changing any props/strings/values will cause compile time errors
const obj = new ValidationTextBoxConstructor({ min: 0, max: 5, disabled: true });
console.log(obj.getObj("test"));
obj.setObj("min", 5);
obj.setObj({ min: 5, max: 0, disabled: false });
TS Playground link
The meat of the solution is the _Properties interface. Extending from this and specifying the generic type T gives you access to using getObj(name: string) where name must be a key of the supplied type, and it will return the type of T[name]. Similarly works with setObj(name: string, value: T[U]), where value must be the type specified by T[name]. The second setObj function also accepts a hash of { key => value }, and calls setObj(key, value) on each key supplied. This also properly type checks the object passed in by allowing any key of T and having it's value be of type T[key].
The *Props interfaces simply define the properties available to them, and are used as the type in _Properties. They should extend from each other if the class that uses them extends from another that extends _Properties.
This also supplies a constructor function that optionally takes the property names and sets them.
The only problem is if a property needs to completely change its type (eg, base class has string property, but inheriting class exposes it as a number), but that is resolvable through generics on the Props interface. Not the cleanest solution, but is rare enough to not matter too much for my needs.