Home > Software engineering >  TypeScript allows unsafe Partial usage in generic
TypeScript allows unsafe Partial usage in generic

Time:08-06

Issue

I'd like to do CRUD stuff on some unknown items (any object with id: number).

For simplicity, let's say that for now we're deling with messages like this:

type Message =
    | { id: number; type: "success"; message: string }
    | { id: number; type: "error"; message: string; status: number };

let messages: Message[] = [{ id: 1, type: "success", message: "Ok" }];

Please notice that if type === "error, status has to be provided (can't be undeifned, since hanlder could use .toPrecision() or other number method).

Update function can look like this (just a basic demo):

function updateItem<T extends { id: number }>(
    items: T[],
    id: number,
    changes: Partial<T>
): T[] {
    return items.map((item) => {
        if (item.id === id) {
            return { ...item, ...changes };
        }

        return item;
    });
}

Let's test it:

messages = updateItem(messages, 1, { type: "error" });
console.log(messages);
// => [{ id: 1, type: "error", message: "Ok" }]

and just like that TypeScript allowed me to create invalid message. It has type === "error", but it doesn't have a status.

It would notify about if I would implement it like this:

function updateMessage(
    items: Message[],
    id: number,
    changes: Partial<Message>
): Message[] {
    return items.map((item) => {
        if (item.id === id) {
            return { ...item, ...changes }; // TS ERROR
        }

        return item;
    });
}

but as I mentioned it will be used used for generic items.

Questions

  • Why TS allows Partial ussage in generic, but not when a type is hard coded?
  • How I could implement it? Idealy it should be able to detect which properties are always optional, and which are required if some property is equal to something (in this example if we pass type: 'error' it would be reasonable to also require status since it may or may not be present in object). My current idea is to do Partial<DeepIntersection<T>>, but I got stuck at DeepIntersection part (TypeScript deep intersection of objects union)

EDIT - example of using Combine

Thanks to awesome sugestion provided by @jcalz here TypeScript deep intersection of objects union I was able to make version with hard coded types work:

function updateMessage(
    items: Message[],
    id: number,
    changes: Partial<Omit<Combine<Message>, "id">>
): Message[] {
    return items.map((item) => {
        if (item.id === id) {
            return { ...item, ...changes }; // No error            
  • Related