Given the example below:
const brands = { mercedes: "mercedes", audi: "audi" } as const;
type Brands = keyof typeof brands;
type MercedesFactory = {
propA: string;
}
type AudiFactory = {
propB: string;
}
type CarProps<TFactory> = {
color: string;
hp: number;
factory: TFactory
}
type Mercedes = {
c180: CarProps<MercedesFactory>,
c220: CarProps<MercedesFactory>,
}
type Audi = {
a3: CarProps<AudiFactory>,
tt: CarProps<AudiFactory>,
}
const mercedes: Mercedes = {
c180:
{
color: "blue",
hp: 120,
factory: { propA: "xx" }
},
c220: {
color: "black",
hp: 150,
factory: { propA: "yy" }
}
}
const audi: Audi = {
a3:
{
color: "blue",
hp: 120,
factory: { propB: "zz" }
},
tt: {
color: "red",
hp: 150,
factory: { propB: "aa" }
}
}
const allCars: Record<Brands, Mercedes | Audi> = {
mercedes,
audi,
}
I want the generic function bellow to return Fabric information for all the blue cars, depends on the brand passed as parameter:
const getAllBlueCars = (brand: Brands) => {
const carBrand = allCars[brand]; // Mercedes | Audi
// object car is not typed correctly (any), and function returns any[]
return Object.values(carBrand).reduce((acc, car) => {
if (car.color === "blue") {
return [...acc, car.factory];
}
return acc;
}, [])
// I'd like car is typed correctly and function returns an MercedesFactory[] or AudiFactory[], depends on which brand was passed like the example bellow
}
But as you can see below, it’s returning any;
const allAudiBlueCarsFabric = getAllBlueCars("audi"); // any
This works fine, car is typed correctly and it returns correct type:
const mercedesFactories = Object.values(mercedes).reduce<MercedesFactory[]>((acc, car) => (car.color === "blue" ? [...acc, car.factory] : acc), []);
// another simple example, using filter
const carMercedesFilterExample = Object.values(mercedes).filter((car) => car.color === "blue");
This is the Chat GPT Solution. The returned value is correct, but car is still typed wrong:
const getAllBlueCars2 = <TBrand extends Brands>(brand: TBrand) => {
const carBrand = allCars[brand];
// it works, but car is still typed as any
return Object.values(carBrand).reduce<((TBrand extends "mercedes" ? MercedesFactory : AudiFactory)[])>((acc, car) => {
if (car.color === "blue") {
return [...acc, car.factory];
}
return acc;
}, []);
};
const allAudiBlueCarsFabric2 = getAllBlueCars2("audi"); // AudiFactory[]
3
Answers
After some hours digging into, I also found myself an acceptable solution.
This is the closest solution I could find.
Your
allCars
value doesn’t have a strongly typed relationship between the keys and values:Since you annotated as
Record<Brands, Mercedes | Audi>
, then it could possibly be a value like{mercedes: audi, audi: mercedes}
. If you want better typings, you’ll need to give it a stronger type like{mercedes: Mercedes, audi: Audi}
, which is what the compiler infers if you don’t annotate at all:Still, even if you do that, the compiler can’t follow the logic:
The
carPropsArray
value is inferred as having typeany[]
. The compiler doesn’t understand thatObject.values(AllCars[K])
should also be generic inK
. The correlation betweenbrand
andcarPropsArray
has been lost.Of course you can just annotate
carPropsArray
as any array type you want, since theany
type is loose enough to permit even crazy things (e.g.,const carPropsArray: number[] = Object.values(carBrand);
would be accepted), but you’re not getting the compiler to compute the correct types for you.The only way I know of to get it the correlation between
brand
andcarPropsArray
to be computed by the compiler is to refactor your types using the technique described in microsoft/TypeScript#47109. Essentially you need to expresscarBrands
as a mapped type over some underlying base type, and then index into it with the generic typeK
to get another generic type out.The way I usually do this is to rename the original object out of the way
and then use it to compute the related base type:
and then rebuild the original type as a mapped type over the base type:
and assign the original object to the new value annotated with the new type:
On the face of it this doesn’t look like anything much has changed, but now the compiler sees an explicit relationship between
AllCars[K]
and `Record<string, CarProps<CarFactories[K]>>.And the implementation of
getAllBlueCars
suddenly becomes a lot more strongly typed:Here,
carPropsArray
is seen as typeCarProps<CarFactories[K]>[]
, and thus when wereduce()
that array with an initial value of typeCarFactories[K][]
, the compiler understands that the implementation is safe, and that the return value is of that generic type. So the type ofgetAllBlueCars
is<K extends Brands>(brand: K) => CarFactories[K][]
Which means when we call it, we get the strongly-typed output you expect:
Playground link to code