Here is my TypeScript code:
type MetadataTags = {
album: boolean;
artist: boolean;
genre: boolean;
year: boolean;
duration: boolean;
contentType: boolean;
artwork: boolean;
directory: boolean;
};
type MusicScanResult<T extends MetadataTags> = {
url: string;
id: string;
title: string;
} & (T['album'] extends true ? { album: string } : {}) &
(T['artist'] extends true ? { artist: string } : {}) &
(T['genre'] extends true ? { genre: string } : {}) &
(T['year'] extends true ? { year: string } : {}) &
(T['duration'] extends true ? { duration: number } : {}) &
(T['contentType'] extends true ? { contentType: string } : {}) &
(T['artwork'] extends true ? { artwork: string } : {}) &
(T['directory'] extends true ? { directory: string } : {});
function scanMediaStore<T extends MetadataTags>(metadata: T): MusicScanResult<T> {
const map = new Map();
map.set("url","sample");
map.set("id","1233");
map.set("title","adarsh");
if(metadata.album) map.set("album","test");
if(metadata.artist) map.set("artist","test");
if(metadata.duration) map.set("duration",0);
if(metadata.contentType) map.set("contentType","test");
if(metadata.directory) map.set("directory","test");
if(metadata.year) map.set("year","test");
if(metadata.genre) map.set("genre","test");
if(metadata.artwork) map.set("artwork","test");
const res = Object.fromEntries(map);
return res;
}
Explanation
The function will return an object which always contain
{
url: string;
id: string;
title: string;
}
Along with this, if any property of the metadata is true then the return value will also contain that property.
So I am trying to write a dynamic type definition for the function return type.
Note that the type of all the properties in the return type is string
, except for property "duration", which is number
.
Problem
When I directly pass the object to the function, the return type is generated correctly, including the properties that are marked as true in the metadata object. However, when I create a separate variable for the metadata object and pass that variable as the argument, I can only access the URL, ID, and title properties from the result. The other properties are not included in the type.
Calling the function:
// This will work as expected.
const ans = scanMediaStore({
album: true,
artist: true,
genre: false,
year: false,
duration: true,
contentType: true,
artwork: false,
directory: false,
});
const metadata: MetadataTags = {
album: true,
artist: true,
genre: false,
year: false,
duration: true,
contentType: true,
artwork: false,
directory: false,
};
// This will not work as expected.
// Here I can only access url, id, title; the rest are not shown.
const ans = scanMediaStore(metadata);
Update: While I appreciate the solutions provided, I'm open to exploring different approaches to achieve the desired type in my code. If anyone has alternative solutions or suggestions to dynamically generate a TypeScript return type based on object properties, please feel free to share. I'm particularly interested in solutions that might offer better type inference or cleaner syntax.
Here is the TypeScript playground.
First of all, let's make sure
metadata
's properties are not widened to justboolean
and the object itself is type-safe, which can be achieved with the combination of the satisfies and const assertion:Now, let's create a type where the keys will be the keys of
MetadataTags
and the values will be what should function return if this property is true:Note that the values could be simplified to just
string
ornumber
if you are sure that you won't add anything else:By using a mapped type and key remapping we will drop the properties that are not true, otherwise, the values will be what's in the
TypeMap
for the specific property:Testing:
Complex
TypeMap
:Simple
TypeMap
:With the simple version, the only thing left is to intersect the result with the rest of the return type, however in the complex version we have to take the values and make sure that they are intersected:
You can read more about the
ValueOf
here andUnionToIntersection
hereTesting:
To make the result type more readable the following utility type from the type-samurai can be used:
This type will remove intersections and will show them under a single object:
Looks good! Let's connect everything together:
Final testing:
playground