约束索引签名(使用泛型)以使 sub-properties 具有匹配的类型

Constraining an index signature (using generics) to make sub-properties have matching types

在 TypeScript (v4.5.4) 中,我试图通过索引签名定义一个 object 类型。 我希望 TypeScript 强制 object 中的某些 sub-properties 具有匹配类型,但允许这些类型在 top-level 属性之间变化。

在下面的 (non-working) 示例中,我希望所有快乐的 driver 都能驾驶他们最喜欢的车型。 driver 最喜欢的车型与其实际车型不匹配会导致 TypeScript 编译器错误。

type CarType = 'Minivan' | 'Sports car' | 'Sedan' ; // | 'Pickup truck' |, etc. Imagine this union type has many possible options, not just three.

type Car = {
  carType: CarType
  // A car probably has many additional properties, not just its type, but those are left out of this minimal example.
  // The solution should be resistant to adding additional properties on `Car` (and the `Driver` type below).
};

type Driver = {
  favoriteCarType: CarType
  car: Car
};

/**
 * Happy drivers drive their favorite car type.
 */
const happyDrivers: { [name: string]: Driver } = {
  alice: {
    favoriteCarType: 'Minivan',
    car: {
      carType: 'Minivan', // ✅ Alice drives her favorite type of car.
    },
  },
  bob: {
    favoriteCarType: 'Sports car',
    car: {
      carType: 'Sedan', /* ❌ Bob doesn't drive his favorite type of car!
        This currently does not throw a compiler error because my types are too permissive, but I want it to. */
    },
  },
};

我已经尝试将泛型应用于索引签名 and/or Car and/or Driver 键入我能想到的所有方式,但我做不到让编译器强制执行约束,即 driver 的 favoriteCarType 必须与他们的 carcarType.

完全匹配

你能帮帮我吗?

您要查找的是以下并集:

type Driver = {
    favoriteCarType: "Minivan";
    car: {
        carType: "Minivan";
    };
} | {
    favoriteCarType: "Sports car";
    car: {
        carType: "Sports car";
    };
} | {
    favoriteCarType: "Sedan";
    car: {
        carType: "Sedan";
    };
}

如果你使 Car 通用(汽车的类型是类型参数),你可以生成这个联合,我们使用自定义映射类型来创建联合的每个组成部分(然后得到一个联合索引回生成的映射类型):


type Car<T extends CarType = CarType> = { carType: T };


type Driver = {
  [P in CarType]: {
    favoriteCarType: P
    car: { carType: P }
  }
}[CarType];

Playground Link

使用映射类型让我走上正轨。请参阅下面的 HappyDriver 类型。

type CarType = 'Minivan' | 'Sports car' | 'Sedan' ; // | 'Pickup truck' |, etc. Imagine this union type has many possible options, not just three.

type Car = {
  carType: CarType
  // A car probably has many additional properties, not just its type, but those are left out of this minimal example.
  // The solution should be resistant to adding additional properties on `Car` (and the `Driver` type below).
};

/**
 * A driver may drive a car of any type, not just their favorite.
 */
type Driver = {
  favoriteCarType: CarType
  car: Car
};

/**
 * A happy driver drives their favorite type of car.
 */
type HappyDriver = {
  [C in CarType]: {
    [K in keyof Driver]: Driver[K] extends Car
      ? { [K2 in keyof Car]: Car[K2] extends CarType
        ? C
        : Car[K2]
      } : Driver[K] extends CarType
        ? C
        : Driver[K] 
  }
}[CarType]

const happyDrivers: { [name: string]: HappyDriver } = {
  alice: {
    favoriteCarType: 'Minivan',
    car: {
      carType: 'Minivan', // ✅ Alice drives her favorite type of car.
    },
  },
  bob: {
    favoriteCarType: 'Sports car',
    car: {
      carType: 'Sedan', /*  Bob doesn't drive his favorite type of car, so this line causes a compiler error, which is what we want to happen. */
    },
  },
};

Playground link.

我已经将 Titian 的答案标记为正确答案,因为它帮助我找到了我正在寻找的答案。