简体   繁体   中英

How do I link/join/relate the types of two function parameters in TypeScript?

Based on the similar question about linking class fields , how do I get TypeScript to recognize that the value of one parameter to a function (whose value is determined at runtime) restricts the type of another?

Example code ( playground ):

class Cat { purr() {/*...*/} }
class Dog { bark() {/*...*/} }
interface TypeMap {
    cat: Cat;
    dog: Dog;
}
const makeSound = function<C extends keyof TypeMap>(
    commonName: C,
    animal: TypeMap[C],
) {
    if(commonName === 'cat') {
        //Error: this.animal is of type 'Cat | Dog',
        //not narrowed to Cat as hoped.
        return animal.purr();
    } else if (commonName === 'dog') {
        //Error: this.animal is of type 'Cat | Dog',
        //not narrowed to Dog as hoped.
        return animal.bark();
    }
}
//The intended type-checking is being done in calling contexts:
const caller = function(cat: Cat, dog: Dog) {
    //Error on next line as expected:
    //Argument of type '"dog"' is not assignable
    //to parameter of type '"cat"'.ts(2345)
    makeSound<'cat'>('dog', dog);
    makeSound<'cat'>('cat', dog); //similar error, as expected
    makeSound<'cat'>('cat', cat); //works
    makeSound('cat', cat); //desired style; cat vs. dog not hard-codable
}

The TS documentation states 'You can declare a type parameter that is constrained by another type parameter.' but the example given is very simple and seems fragile in terms of ability to generalize to a slightly more complex example like this one.

You are manually checking commonName against individual cases, and TypeScript really doesn't have good support for type checking the body of makeSound() if it is generic . Even though commonName is of the generic type C , checking commonName only narrows the type of commonName itself, it does not further constrain C . So unfortunately, even if the compiler knows that commonName is "cat" , it does not know that C is "cat" . It can still be "cat" | "dog" "cat" | "dog" , and thus worries about the (admittedly unlikely) possibility of a call like

makeSound(
  Math.random() < 0.99 ? "cat" : "dog", 
  Math.random() < 0.99 ? dog : cat
); // no error?!

We have failed to actually make the link we intended to make, and even though it's usually good enough (I mean, really, who passes cross-correlated unions into functions like that?), the compiler is unwilling to type check the body.

There are various feature requests asking for an improvement here. For example, microsoft/TypeScript#27808 wants to make it possible to say something C extends oneof keyof TypeMap so that the full union type keyof TypeMap would be rejected. For now though nothing is implemented, and if we want to write your code we will need to refactor (or else pepper your code with type assertions to suppress the error... but let's investigate refactoring.)


Because the return type of makeSound() does not depend on C , you can refactor your call signature to a non-generic version that enforces the actual constraint you have. Essentially the parameter tuple , [commonName, animal] forms a discriminated union where the property at index 0 is the discriminant , and checking it will narrow the type of the property at index 1 . You can compute this union type like this:

type MakeSoundParam = { [K in keyof TypeMap]:
    [commonName: K, animal: TypeMap[K]]
}[keyof TypeMap];

/* type MakeSoundParam = 
  [commonName: "cat", animal: Cat] | 
  [commonName: "dog", animal: Dog] */

TypeScript supports control flow analysis for destructured discriminated unions , which means you can rewrite your function as:

const makeSound = function (...[commonName, animal]: MakeSoundParam) {
    if (commonName === 'cat') {
        return animal.purr(); // okay
    } else if (commonName === 'dog') {
        return animal.bark(); // okay
    }
}

That compiles without error. And just to be sure that makeSound() still behaves properly from the caller's side:

const caller = function (cat: Cat, dog: Dog) {
    makeSound('cat', cat); // ok
    makeSound('dog', dog); // ok
    makeSound('cat', dog); // error
    makeSound('dog', cat); // error
    makeSound(
      Math.random() < 0.99 ? "cat" : "dog", 
      Math.random() < 0.99 ? dog : cat
 ); // error
}

Looks good, and even the cross-correlated call is rejected.

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