0

I have several code blocks that look like this. Is there any better way to do this with generics or some other typescript feature?

    const { authors } = book
    
    if (authors?.author) {
      if (Array.isArray(authors.author)) {
        book.authors = authors.author
      } else {
        book.authors = [authors.author]
      }
    }

and

    const { bookLinks } = book
    
    if (bookLinks?.bookLink) {
      if (Array.isArray(bookLinks.bookLink)) {
        book.bookLinks = bookLinks.bookLink
      } else {
        book.bookLinks = [bookLinks.bookLink]
      }
    }

I want to create a function that takes two parameters, say ('authors', 'author') or ('bookLinks', 'bookLink') and replace the variables, in the above code.

2
  • Is this JavaScript code or TypeScript code? If the latter, what's the type of book? Commented Jul 25, 2020 at 2:34
  • Its TypeScript and the type of book is unknown, as I'm fetching from external API. But this pattern is repeated in several places, so I want to make this generic Commented Jul 25, 2020 at 3:01

1 Answer 1

1

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

Sign up to request clarification or add additional context in comments.

2 Comments

That works. Could you please suggest a good resource or a book for advanced typescript. The use-case, I'm just cleaning up the data from the external API, to make it proper JSON
Thank you @jcalz. I see a lot of effort went into the answer.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.