Определение TypeScript для объединения объектов, но предотвращает слияние разных типов в свойствах объекта - PullRequest
1 голос
/ 18 апреля 2020

Я хочу написать функцию, которая расширяет объект другим объектом. Например: мы будем называть один объект src, а другой ext. Функция должна возвращать (копию) объекта src, но глубоко (рекурсивно) расширяет объект объектом ext. Если тип данных одного (подчиненного) свойства ext не соответствует типу src (вспомогательного) свойства, функция должна игнорировать значение ext. Новые свойства из ext (не существуют в src) будут добавлены к объекту результата.

Для лучшего понимания здесь приведен полный пример:

/** A helper definition to build indexable objects */
interface indexObject<T> {
    [key: string]: T
}

type SafelyMergedObject<Src extends indexObject<any>, Ext extends indexObject<any>> = {
    // The searched type definition
}

const src= {
    a: "a",
    b: "b",
    c: false,
    d: {
        a: "Alice",
        b: "Bob",
    }
}
const ext = {
    a: ["a"],
    b: 1,
    c: true,
    d: {
        a: "Ann",
        c: "Clair",
    },
    e: "New value",
}
const result: SafelyMergedObject<typeof src, typeof ext> = {
    a: "a", /** Only string should be allowed here because
                it's the property type of the source (Src) type */
    b: "b", /** Same as above (`a` property) */
    c: true,/** Since `c` has the same data type the function
                should return the property value of the `ext`
                object */
    d: {    /** Same data type in both objects */
        a: "Ann",   /** new value from `ext` */
        b: "Bob",   /** copied value from `src` */
        c: "Clair", /** new property from `ext` */
    },
    e: "New Value", /** new property from `ext` */
}

TypeScript Playground Link

Я знаю, как написать функцию. Это легко, но я не знаю, как написать это определение типа. Это вообще возможно?

Поведение вывода типа TypeScript по умолчанию не подходит для моей проблемы, поскольку типы являются рекурсивными и более сложными, чем простой тип object. Я буду использовать функцию, например, для загрузки пользовательских настроек c в мое приложение. Пользовательская конфигурация может быть повреждена. Поэтому я должен объединить конфигурацию по умолчанию с пользовательской настройкой c.

1 Ответ

1 голос
/ 19 апреля 2020

Я интерпретирую то, что вы запрашиваете, следующим образом: для типов объектов T и U тип объекта SafelyMergedObject<T, U> должен иметь те же ключи, что и T & U, но с некоторыми отличиями от свойства типы. Если ключ K существует только в T или U, но не в обоих, используйте свойство как есть (так что это то же самое, что и T & U). Если ключ K существует в T и U и хотя бы один из двух типов свойств не является объектом, используйте тип свойства из T и игнорируйте свойство из U. Если ключ K существует в T и U, а является типом объекта в T и U, затем вернитесь в это свойство с помощью SafelyMergedObject<T[K], U[K]>.

Это переводится в нечто вроде:

type SafelyMergedObject<T, U> = (
    Omit<U, keyof T> & { [K in keyof T]:
        K extends keyof U ? (
            [U[K], T[K]] extends [object, object] ?
            SafelyMergedObject<T[K], U[K]>
            : T[K]
        ) : T[K] }
) extends infer O ? { [K in keyof O]: O[K] } : never;

Здесь мы сначала выводим Omit<U, keyof T>, то есть свойства из U, которые не существуют в T. Затем мы проходим ключи T и выводим T[K], если свойство не в U, или если оно находится в U, но хотя бы один из T[K] или U[K] не является объект.

Единственный "трюк" здесь - extends infer O ? {[K in keyof O]: O[K]} : never. Все, что это делает, - это «предварительно укрепляет» или «расширяет» тип объекта, перебирая все ключи и объединяя результат в один тип объекта.

Давайте посмотрим на это в действии с вашими src и ext значения:

type Result = SafelyMergedObject<typeof src, typeof ext>;

Если вы наводите курсор на это с IntelliSense, вы увидите:

type Result = {
    e: string;
    a: string;
    b: string;
    c: boolean;
    d: {
        c: string;
        a: string;
        b: string;
    };
}

, что, я думаю, то, что вы хотели. Обратите внимание, что если бы я не включил строку extends infer O..., тип Result был бы оценен как:

type Result = Pick<{
    a: string[];
    b: number;
    c: boolean;
    d: {
        a: string;
        c: string;
    };
    e: string;
}, "e"> & {
    a: string;
    b: string;
    c: boolean;
    d: SafelyMergedObject<{
        a: string;
        b: string;
    }, {
        a: string;
        c: string;
    }>;
}

, что значительно сложнее для понимания, хотя это тот же тип.


Обратите внимание, что, вероятно, существуют всевозможные крайние случаи, которые возникнут, если вы используете выше SafelyMergedObject<T, U> в различных ситуациях. Вам нужно будет решить, как вы хотите, чтобы результат выглядел в этих ситуациях, и, возможно, изменить определение, чтобы это произошло. Так что будьте осторожны.

Хорошо, надеюсь, это поможет; удачи!

Детская площадка ссылка на код

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...