2

I'm currently attempting to add some typings to a library that relies a lot on inheritance. The general hierarchy looks something like:

BaseWidget --> TextBox --> ValidationTextBox

BaseWidget provides a JS function called getObj(name) which maps to getObj(name: string): any in TS and effectively looks on the current object for a function called _get<name>, executes it, and returns the result.

These 'properties' are exposed on the individual classes, and are inherited between classes such that ValidationTextBox has access to all the properties on TextBox. I'm wondering if this is possible to add typings similar to what I've tried below without having to re-define the overloads in each class.

interface BaseWidget {
    getObj(name: string): any
}

interface TextBox extends BaseWidget {
    getObj(name: "B"): string
}

interface ValidationTextBox extends TextBox {
    getObj(name: "C"): boolean
    // getObj(name: "B"): string; // Adding this would make it compile, but obviously not ideal in the least
    // getObj(name: string): any // Adding this also compiles, but I lose type information for 'getObj("B")'
}

declare const obj: ValidationTextBox;
console.log(obj.getObj("B")); // Error: Argument of type '"B"' is not assignable to parameter of type '"C"'

TS Playground link

The error with this current solution is Interface 'ValidationTextBox' incorrectly extends interface 'TextBox'. since "B" is not assignable to "C" in getObj(...).

Help appreciated!

2 Answers 2

1

There might be several solution. The simplest would be to make the compiler happy by specifying the extra overloads but without re-writing them using type queries:

interface ValidationTextBox extends TextBox {
    // The extra overload with C, and whatever overloads are in the base class (TextBox['getObj'])
    getObj: ((name: "C") => boolean) & TextBox['getObj'];
}

Another solution is to make the types generic to allow us to add to the type of the name parameter without actually overwriting the method:

interface BaseWidget<TName = string> {
    getObj(name: TName): any
}


// TName can be used to add to the getName overload in derived interfaces
interface TextBox<TName = never> extends BaseWidget<TName | "B"> {
}

// TName can be used to add to the getName overload in derived interfaces
interface ValidationTextBox<TName = never> extends TextBox<TName | "C"> {
}

declare const obj: ValidationTextBox;
console.log(obj.getObj("B"));
console.log(obj.getObj("C"));
Sign up to request clarification or add additional context in comments.

2 Comments

Incredible! Thank you. I didn't know you could use type queries like that in interfaces. I'll play around with it a bit more, but I believe the first option is what I'm looking for.
Actually, I came across a couple problems with these methods. The first method gets a bit unwieldy when there are a lot of properties on multiple classes (ends up having lots of type intersections on interface properties), and the generic method only allows specifying the name but leaves the return type as any. I've posted my own answer below inspired from the generic method you've mentioned here.
0

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.

Comments

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.