Введите безопасное слияние типов объектов подписи индекса в машинописи - PullRequest
1 голос
/ 11 января 2020

Этот вопрос и ответ охватывает литералы объектов , но ответ не работает при использовании типов объектов подписи индекса. Например:

type UniqueObject<T, U> = { [K in keyof U]: K extends keyof T ? never : U[K] }

export function mergeUnique <T, U, V> (
  a: T,
  b?: UniqueObject<T, U>,
  c?: UniqueObject<T & U, V>,
) {
  return {
    ...a,
    ...b,
    ...c,
  }
}

type Obj = { [index: string]: number | undefined }
const a: Obj = { a: undefined }
const b: Obj = { b: 3 }

// should all pass
const res01 = mergeUnique({ a: undefined }, { b: 3 })
const res02 = mergeUnique({ a: undefined }, b)
const res03 = mergeUnique(a, { b: 3 })                 // errors incorrectly ❌ `Type 'number' is not assignable to type 'never'`
const res04 = mergeUnique(a, b)                        // errors incorrectly ❌ `Type 'undefined' is not assignable to type 'never'`
const res05 = mergeUnique({ b: 3 }, { a: undefined })
const res06 = mergeUnique(b, { a: undefined })         // errors incorrectly ❌ `Type 'undefined' is not assignable to type 'never'`
const res07 = mergeUnique({ b: 3 }, a)
const res08 = mergeUnique(b, a)                        // errors incorrectly ❌ `Argument of type 'Obj' is not assignable to parameter of type 'UniqueObject<Obj, { [x: string]: ...; }>'`

// should all fail
const res09 = mergeUnique({ a: undefined }, { a: undefined })
const res10 = mergeUnique({ a: undefined }, a)         // passes incorrectly ❌
const res11 = mergeUnique(a, { a: undefined })
const res12 = mergeUnique(a, a)                        // errors correctly ? but reason wrong: `Argument of type 'Obj' is not assignable to parameter of type 'UniqueObject<Obj, { [x: string]: ...; }>'`

Код

1 Ответ

1 голос
/ 11 января 2020

Хотя существуют некоторые методы для манипулирования типами с помощью сигнатур индекса (см., Например, этот ответ ), указанная проверка c, которую вы хотите выполнить, здесь невозможна. Если значение аннотируется как тип string, то компилятор не сузит его до строкового литерала типа , даже если вы инициализируете его строковым литералом:

const str: string = "hello"; // irretrievably widened to string
let onlyHello: "hello" = "hello";
onlyHello = str; //error! string is not assignable to "hello"

В приведенном выше примере переменная string str инициализируется значением "hello", но вы не можете присвоить ее переменной типа "hello"; компилятор навсегда забыл, что значение str является строковым литералом "hello".

Это «забывчивое» расширение верно для любой аннотации типа, не являющегося объединением. Если тип является объединением, компилятор фактически сужает тип переменной при присваивании, по крайней мере, до тех пор, пока переменная не будет переназначена:

const strOrNum: string | number = "hello"; // narrowed from string | number to string
let onlyString: string = "hello";
onlyString = strOrNum; // okay, strOrNum is known to be string

К сожалению, ваш тип Obj не является типом объединения , А поскольку он имеет подпись индекса string, компилятор будет знать только, что переменная, аннотированная как Obj, будет иметь ключи string и не будет помнить литеральное значение этих ключей, даже если он инициализируется литералом объекта со строковыми литеральными ключами:

const obj: Obj = { a: 1, b: 2 }; // irretrievably widened to Obj
let onlyAB: { a: 1, b: 1 } = { a: 1, b: 1 };
onlyAB = obj; // error! Obj is missing a and b

Таким образом, переменные a и b, которые были аннотированы как тип Obj, известны компилятору только как тип Obj. Он забыл какие-то отдельные свойства внутри них. С точки зрения системы типов a и b идентичны.

И, таким образом, независимо от того, в какие безумные игры типа я пытаюсь играть с подписью для mergeUnique(), ничего, что я могу сделать, не сделает это так, что mergeUnique(a, b) успешно, а mergeUnique(a, a) неудачно; типы a и b являются идентичными типами без объединения; компилятор не может их различить.


Если вы хотите, чтобы компилятор запомнил отдельные ключи в a и b, вам не следует комментировать их, но позвольте компилятору вывести их. Если вы хотите убедиться, что a и b можно присвоить Obj без фактического расширения их до Obj, вы можете сделать вспомогательную функцию generi c, чтобы сделать это:

const asObj = <T extends Obj>(t: T) => t;

Функция asObj() просто возвращает то же значение, которое она получает в качестве аргумента, и не меняет свой предполагаемый тип. Но поскольку T ограничено с до Obj, оно будет успешным только в том случае, если объект можно назначить на Obj:

const a = asObj({ a: undefined }); // {a: undefined}
const b = asObj({ b: 3 }); // {b: number}
const c = asObj({ c: "oopsie" }); // error!

Теперь у вас есть a и b узких типов с известными строковыми литеральными ключами свойств (и c с ошибкой компилятора, потому что "oopsie" не является `числом | неопределенным). И, таким образом, остальная часть вашего кода ведет себя так, как нужно:

// these all succeed
const res01 = mergeUnique({ a: undefined }, { b: 3 })
const res02 = mergeUnique({ a: undefined }, b)
const res03 = mergeUnique(a, { b: 3 })
const res04 = mergeUnique(a, b)
const res05 = mergeUnique({ b: 3 }, { a: undefined })
const res06 = mergeUnique(b, { a: undefined })
const res07 = mergeUnique({ b: 3 }, a)
const res08 = mergeUnique(b, a)
// these all fail
const res09 = mergeUnique({ a: undefined }, { a: undefined })
const res10 = mergeUnique({ a: undefined }, a)      
const res11 = mergeUnique(a, { a: undefined })
const res12 = mergeUnique(a, a)                    

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

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

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