I'm not 100% sure I'm following the use case, but maybe your function could look like this:
function fixBook<K extends PropertyKey>(
book: any, key: K, subKey: PropertyKey
): asserts book is { [P in K]?: unknown[] } {
if (!book) throw new Error("This is not an object");
const { [key]: prop } = book;
if (prop?.[subKey]) {
if (Array.isArray(prop[subKey])) {
book[key] = prop[subKey];
} else {
book[key] = [prop[subKey]];
}
}
}
This should behave similarly to your above code. It is an assertion function, which means that after you call it, it will narrow the type of the input object so you can access its properties.
Example:
const book: unknown = {
authors: { author: "A" },
bookLinks: { bookLink: ["b", "c"] }
}
Here we have book of type unknown... the annotated unknown makes the compiler forget the actual type, so this should replicate the situation in which you get book from an API and don't know what type it is:
book.authors.author; // error! object is of type unknown
Now we call fixBook() twice. First:
fixBook(book, "authors", "author");
After that statement, book has been narrowed from unknown to {authors?: unknown[]}. (Note that it's not {authors: string[]}, since the compiler has no idea what type book?.authors?.author will be. Following the different code paths, I think after you run the function, the particular property will either be undefined or some array of unknown type.) And then:
fixBook(book, "bookLinks", "bookLink");
After that statement, book has been narrowed further to {authors?: unknown[]} & {bookLinks?: unknown[]}. We can verify by accessing the authors and bookLinks properties:
console.log(book.authors?.join(",")); // A
console.log(book.bookLinks?.join(",")); // b,c
Looks reasonable.
Playground link to code
book?