Распространение оператора и необязательные поля. Как вывести правильный тип - PullRequest
0 голосов
/ 11 марта 2020

Предположим, у вас есть объект с необязательным обязательным полем:

interface MyTypeRequired { 
    value: number;
}

И вы хотите обновить его полями другого объекта с необязательным полем:

interface MyTypeOptional { 
    value?: number;
}

Итак, вы go впереди и создаете функцию:

function createObject(a: MyTypeRequired, b: MyTypeOptional) {
    return { ...a, ...b };
}

Каким будет предполагаемый тип возврата этой функции?

const a = createObject({ value: 1 }, { value: undefined });

Эксперименты показывают, что она будет соответствовать интерфейсу MyTypeRequired даже если второй спред имеет необязательное поле.

Если мы изменим порядок, тип вывода не изменится, даже если тип среды выполнения будет другим.

function createObject(a: MyTypeRequired, b: MyTypeOptional) {
    return { ...b, ...a };
}

Почему такое поведение в TypeScript и как обойти эту проблему?

Ответы [ 3 ]

1 голос
/ 11 марта 2020

Внимание! Это моя интерпретация (теория) того, что происходит. Это не подтверждено.


Похоже, что проблема связана с обработкой необязательных полей.

Когда вы определяете этот тип:

interface MyTypeOptional { 
    value?: number;
}

TypeScript ожидает, что значение будет иметь тип number | never.

Так что при его распространении

const withOptional: MyTypeOptional = {};
const spread = { ...withOptional };

Тип объекта распространения должен быть либо { value: never | number }, либо { value?: number }.

К сожалению, из-за неоднозначного использования необязательного поля и неопределенного TypeScript выводит объект распространения следующим образом:

value?: number | undefined

Таким образом, необязательный тип поля приводится к number | undefined при распространении.

Что не имеет смысла, так это то, что когда мы распространяем не необязательный тип, он переопределяет необязательный тип:

interface MyTypeNonOptional { 
    value: number;
}
const nonOptional = { value: 1 };
const spread2 = { ...nonOptional, ...withOptional }

Похоже на ошибку? Но нет, здесь TypeScript не приводит дополнительное поле к number | undefined. Здесь он преобразуется в number | never.

. Мы можем подтвердить это, изменив необязательный параметр на явный |undefined:

interface MyTypeOptional { 
    value: number | undefined;
}

Здесь следующий спред:

const withOptional: MyTypeOptional = {} as MyTypeOptional;
const nonOptional = { value: 1 };
const spread2 = { ...nonOptional, ...withOptional }

будет выведено как { value: number | undefined; }

И когда мы изменим его на необязательный или неопределенный:

interface MyTypeOptional { 
    value?: number | undefined;
}

, оно снова будет сломано и выведено как { value: number }


Так в чем же обходной путь?

Я бы сказал, не используйте дополнительные поля, пока команда TypeScript их не исправит.

Это имеет несколько последствий:

  • Если вам нужно, чтобы ключ объекта был пропущен, если он не определен, используйте omitUndefined(object)
  • . Если вам нужно использовать Partial, ограничьте его использование фабрикой: <T>createPartial (o: Partial<T>) => Undefinable<T>. Так что результат createPartial<MyTypeNonOptional>({}) будет выводиться как { value: undefined } или Undefinable<MyTypeNonOptional>.
  • Если вам нужно, чтобы значение поля было unset, используйте для этого null.

Существует открытый вопрос об этом ограничении дополнительных полей: https://github.com/microsoft/TypeScript/issues/13195

1 голос
/ 11 марта 2020

Развертываются типы объектов следующим образом :

Сигнатуры вызовов и конструкций удаляются, сохраняются только свойства, не относящиеся к методам, а для свойства с тем же именем используется тип самого правого свойства .

Литералы объектов с generi c выражениями распространения теперь создают типы пересечений , аналогично функции Object.assign и литералам JSX. Например:

Это работает хорошо, пока у вас не будет необязательного свойства для самого правого аргумента. {value?: number; } может означать либо 1) отсутствующее свойство, либо 2) свойство со значением undefined - TS не может различать guish в этих двух случаях с необязательным модификатором ?. Давайте рассмотрим пример:

const t1: { a: number } = { a: 3 }
const u1: { a?: string } = { a: undefined }

const spread1 = { ...u1 } // { a?: string | undefined; }
const spread2 = { ...t1, ...u1 } // { a: string | number; }
const spread3 = { ...u1, ...t1 } // { a: number; }
spread1

имеет смысл, ключ свойства a может быть установлен или не установлен. У нас нет другого выбора, кроме как выразить последний типом undefined. Тип

spread2

a определяется самым правым аргументом. Таким образом, если a в u1 существует, это будет string, в противном случае оператор распространения просто получит a из первого аргумента t1 с типом number. Так что string | number также имеет смысл. Примечание : здесь нет undefined. Предполагается, что TS вообще не существует, или это string. Результат будет другим, если мы дадим a явный тип значения свойства undefined:

const u2 = { a: undefined }
const spread4 = { ...t1, ...u2 } // { a: undefined; }
spread3

Последний прост: a из t1 перезаписывает a из u1, поэтому мы получаем number назад.


Я не буду делать ставку на исправление , поэтому здесь возможен обходной путь с отдельным типом Spread и функция:

type Spread<L, R> = Pick<L, Exclude<keyof L, keyof R>> & R;

function spread<T, U>(a: T, b: U): Spread<T, U> {
    return { ...a, ...b };
}

const t1_: { a: number; b: string }
const t2_: { a?: number; b: string }
const u1_: { a: number; c: boolean }
const u2_: { a?: number; c: boolean }

const t1u2 = spread(t1_, u2_); // { b: string; a?: number | undefined; c: boolean; }
const t2u1 = spread(t2_, u1_); // { b: string; a: number; c: boolean; }

Надеюсь, это имеет смысл! Вот образец для приведенного выше кода.

1 голос
/ 11 марта 2020

Обходной путь может быть сделан функцией уровня типа. Обратите внимание:

interface MyTypeRequired { 
    a: number;
    value: number;
}

interface MyTypeOptional { 
    b: string;
    value?: number;
}

type MergedWithOptional<T1, T2> = {
    [K in keyof T1]: K extends keyof T2 ? T2[K] extends undefined ? undefined : T2[K] : T1[K] 
} & {
    [K in Exclude<keyof T2, keyof T1>]: T2[K]
}

function createObject(a: MyTypeRequired, b: MyTypeOptional): MergedWithOptional<MyTypeRequired, MyTypeOptional> {
    return { ...b, ...a };
}

Я добавил дополнительные поля, чтобы проверить поведение. Что я делаю, так это когда второй объект имеет необязательное поле (возможно undefined), а затем рассмотрим это, добавив это в объединение, так что результат будет T1[K] | undefined. Вторая часть просто объединяет все остальные поля, которые находятся в T2, но не в T1.

Подробнее:

  • K extends keyof T2 ? T2[K] extends undefined ? undefined : T2[K] : T1[K], если второй объект имеет этот ключ и его значение может быть неопределенным, затем добавить неопределенное к типу значения, и оно добавляется и не заменяется, потому что именно так условные типы ведут себя для типов объединения
  • Exclude<keyof T2, keyof T1> - использовать только ключи, которые не в первом объекте
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...