简体   繁体   中英

How do I link/join/relate the types of two class fields in TypeScript?

How do I get TypeScript to recognize that the value of one field in a class restricts the type of another?

Example code ( playground ):

class Cat { purr() {/*...*/} }
class Dog { bark() {/*...*/} }
interface TypeMap {
    cat: Cat;
    dog: Dog;
}
class Pet<C extends keyof TypeMap> {
    commonName: C;
    animal: TypeMap[C];
    constructor(commonName : C, animal: TypeMap[C]) {
        this.commonName = commonName;
        /*
        //Dropping the parameter and trying to create the object here doesn't work:
        if(commonName === 'cat') {
            this.animal = new Cat();
        } //...
        // because Type 'Cat' is not assignable to type 'Cat & Dog'.
        */
        this.animal = animal;
    }
    makeSound() {
        if(this.commonName === 'cat') {
            //Error: this.animal is of type 'Cat | Dog',
            //not narrowed to Cat as hoped.
            return this.animal.purr();
        } else if (this.commonName === 'dog') {
            //Error: this.animal is of type 'Cat | Dog',
            //not narrowed to Dog as hoped.
            return this.animal.bark();
        }
    }
}

The type of restriction shown in makeSound() is an example of what I'm trying to accomplish - where you should be able to inspect commonName to learn more about the type of this.animal in a way that narrows its type.

A related question about function parameters is here .

The commonName and animal properties of Pet naturally make Pet a discriminated union where the commonName property is the discriminant . Your implementation of makeSound() would type check without a problem if Pet were such a discriminated union.

The only problem is that Pet is a class , and class instance types need to be definable as an interface , and interfaces cannot be a union at all, let alone a discriminated union.

Instead you are trying to represent Pet as generic . That captures the constraint for commonName and animal more or less (well, C can be a union type itself, and this kind of breaks things, but for this situation we will ignore the potential unsoundness. For our purposes here we can assume that C will either be "cat" or "dog" or any other single member of keyof TypeMap ). But the compiler will not allow makeSound() to type check.

So we will have to refactor (or pepper things with type assertions to suppress errors, but let's not do that).


One potential refactoring is to abstract the check in makeSound() to a lookup so that we are not doing case-by-case control flow. It would look something like this:

const petSound = {
    cat: (cat: Cat) => cat.purr(),
    dog: (dog: Dog) => dog.bark()
}

class Pet<C extends keyof TypeMap> {
    makeSound() {
        petSound[this.commonName](this.animal); // error
    }
}

The problem is that the compiler can't follow the correlation between the type of petSound[this.commonName] and this.animal . This is the problem I've been calling "correlated unions", as described in microsoft/TypeScript#30581 . The fix for this was implemented in microsoft/TypeScript#47109 and involves giving petSound a distributive object type where the compiler can follow the correlation. It looks like this:

const petSound: { [C in keyof TypeMap]: (animal: TypeMap[C]) => void } =
{
    cat: cat => cat.purr(),
    dog: dog => dog.bark()
};

Merely by giving petSound a mapped type that iterates over keyof TypeMap , the above makeSound() compiles with no error:

class Pet<C extends keyof TypeMap> {
    makeSound() {
        petSound[this.commonName](this.animal); // okay
    }
}

Looks good!

(Note that this is still unsound; someone could specify C as keyof TypeMap and then give a completely wrong animal property for a given commonName property. But TypeScript intentionally ignores this unsoundness in order for this correlated union thing to work. See microsoft/TypeScript#48730 for more information).

Playground link to code

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