I have these interface
interface CommonAnimalProps {
common: {
size: number,
weight: number,
origin: string,
}
}
interface DogProps extends CommonAnimalProps {
name: string
}
interface CatProps extends CommonAnimalProps {
furious: boolean
}
interface BirdProps extends CommonAnimalProps {
canFly: boolean
}
export enum AnimalType {
dog = "dog",
cat = "cat",
bird = "bird",
}
export interface AnimalTypeToAnimalPropsMap {
[AnimalType.dog]: (DogProps)[],
[AnimalType.cat]: (CatProps)[],
[AnimalType.bird]: (BirdProps)[]
}
export type AllAnimalValues<T> = T[keyof T]
export type AllAnimalTypesArray = AllAnimalValues<AnimalTypeToAnimalPropsMap>
export type AllAnimalTypes = AllAnimalValues<AnimalTypeToAnimalPropsMap>[0]
export type AllAnimalTypesArrayDiscriminating = AllAnimalTypes[]
This AllAnimalTypesArrayDiscriminating
results in an array looking like this
const foo: AllAnimalTypesArrayDiscriminating = [
{
name: "Bello",
common: {
size: 10,
weight: 25,
origin: "Knowhwere"
},
},
{
furious: true,
common: {
size: 3,
weight: 5,
origin: "Anywhere"
},
},
{
canFly: false,
common: {
size: 39,
weight: 50,
origin: "Somewhere"
},
},
]
Is it possible to use AllAnimalTypesArrayDiscriminating
, and create a new type/interface from it (AllAnimalTypesArrayDiscriminatingModified
), where each element in the resulting array bar
has another prop (color: string
) added to CommonAnimalProps
so that the result looks like this
const bar: AllAnimalTypesArrayDiscriminatingModified = [
{
name: "Bello",
common: {
size: 10,
weight: 25,
origin: "Knowhwere",
color: 'red',
},
},
{
furious: true,
common: {
size: 3,
weight: 5,
origin: "Anywhere",
color: 'brown',
}
}, {
canFly: false,
common: {
size: 39,
weight: 50,
origin: "Somewhere",
color: 'white',
},
}
];
Note, I cannot modify the original CommonAnimalProps
to add color
as an optional parameter. I also cannot edit DogProps, CatProps, BirdProps
. I am looking for a way that uses AllAnimalTypesArrayDiscriminating
(or any of the other types which I did not exclude) to add this property. Is there a way to do this?
By far the easiest approach is to just use an intersection to add the appropriately nested member to AllAnimalTypes
:
type AllAnimalTypesModified = AllAnimalTypes & { common: { color: string } };
type AllAnimalTypesArrayDiscriminatingModified = AllAnimalTypesModified[];
That gives you the desired behavior in your example:
const bar: AllAnimalTypesArrayDiscriminatingModified = [
{
name: "Bello",
common: { size: 10, weight: 25, origin: "Knowhwere", color: 'red' }
},
{
furious: true,
common: { size: 3, weight: 5, origin: "Anywhere", color: 'brown' }
},
{
canFly: false,
common: { size: 39, weight: 50, origin: "Somewhere", color: 'white' }
}
];
But note that this doesn't actually put that member down inside each member of the AllAnimalTypes
union. All it's doing is saying that, in addition to being an AllAnimalTypes
value, it should also have a string
-valued property at its common
property.
So while, for example, DogProps & { common: { color: string } }
is more or less equivalent to {name: string, common: { size: number, weight: number, origin: string, color: string } }
, it's not represented that way, and depending on what type manipulations you perform, that difference could be noticeable.
If you really wanted to compute that type so that each member of the resulting union has a nested property that lives directly in the same object type as the rest of the common properties, you need to do more type juggling. Here's one way to do it:
type AllAnimalTypesModified = AddCommonColor<AllAnimalTypes>;
where AddCommonColor
is a utility type like
type AddCommonColor<T extends CommonAnimalProps> =
T extends unknown ? {
[K in keyof T]: K extends "common" ? T[K] & { color: string } : T[K]
} : never;
This is a distributive conditional type. The type T extends unknown ? ⋯T⋯ : never
looks like a no-op, since all types T
extend unknown
, but the point of this type isn't to perform a check, but to distribute the ⋯T⋯
piece over unions in T
. That is, we want AddCommonColor<X | Y | Z>
to evaluate to AddCommonColor<X> | AddCommonColor<Y> | AddCommonColor<Z>
.
The actual type we're computing for a given union member is { [K in keyof T]: K extends "common" ? T[K] & { color: string } : T[K] }
, a mapped type that leaves all properties alone except for common
, to which {color: string}
is intersected.
If we now inspect AllAnimalTypesModified
we get
type AllAnimalTypesModified = {
name: string;
common: {
size: number;
weight: number;
origin: string;
} & {
color: string;
};
} | {
furious: boolean;
common: {
size: number;
weight: number;
origin: string;
} & {
color: string;
};
} | {
canFly: boolean;
common: {
size: number;
weight: number;
origin: string;
} & {
color: string;
};
}
where the color
property lives down inside the common
property of each union member. Even this might not be the way you'd like to see it; it's possible to get rid of that intersection like this:
type AddCommonColor<T extends CommonAnimalProps> =
T extends unknown ? {
[K in keyof T]: K extends "common" ? {
[P in "color" | keyof T[K]]: P extends keyof T[K] ? T[K][P] : string
} : T[K]
} : never;
which produces
type AllAnimalTypesModified = {
name: string;
common: {
color: string;
size: number;
weight: number;
origin: string;
};
} | {
furious: boolean;
common: {
color: string;
size: number;
weight: number;
origin: string;
};
} | {
canFly: boolean;
common: {
color: string;
size: number;
weight: number;
origin: string;
};
}
but that might be overkill for your use case.