typescript: не работает ? при работу с типом из нескольких интерфейсов

Рейтинг: 0Ответов: 1Опубликовано: 10.04.2023

Есть такой код:

interface IData1 {
  value: number
}

interface IData2 {
  text: string
}

type IData = IData1 | IData2;

const data1: IData1 = {
    value: 15
}

const data2: IData2 = {
    text: 'data2 test'
}

const data_n: IData = data1;
const data_t: IData = data2;

При попытке обратиться к значению, которого нет вылезает ошибка

const num: number = data_n.value; // OK, работает
const txt: string = data_t.text;  // OK, работает 
const num2: number | undefined = data_t?.value; // FAILED, Property 'value' does not exist on type 'IData2'.

вроде бы при использовании конструкции ?. если поле отсутствует, то должно вернуться undefined

почему этого не происходит и как эту проблему можно исправить?

Ответы

▲ 1

Если я все правильно понимаю, то проблема в том, что такая конструкция type IData = IData1 | IData2;, фигурально выражаясь, создает состояние "суперпозиции", когда IData одновременно является обоими интерфейсами. Но, как только вы инициализируете переменную, её тип становится детерминированным (либо IData1, либо IData2).

Т.е., по сути, вот после выполнения этого кода тип переменных становится известен.

const data_n: IData = data1; // --> data_n: IData1
const data_t: IData = data2; // --> data_t: IData2

Если вам нужно, чтобы тип реализовывал оба инерфейса сразу, а не 1 или 2, то надо вот так делать:

type IData = IData1 & IData2;

Но у такого варианта тоже есть недостаток, т.к. теперь нужно приводить data1 и data2 к новому типу IData:

const data_n: IData = data1 as IData
const data_t: IData = data2 as IData

Дисклеймер: Могу ошибаться. Это чисто мои рассуждения над вашим вопросом.


UPD

к коментарию.

Возможно, что это и правда микроскопом по гвоздям. И я, возможно, тоже немного накрутил... :)

Конкретно для реакта не подскажу, но в целом, возможно стоит воспользоваться Generic'ами. Как-то так:

Это, типа, ваши компоненты

const component1 = (data: TData<number>) => {
  return data
}

const component2 = (data: TData<string>) => {
  return `text: ${data}`
}


const component3 = (data: TData<IRange>) => {
  return `range: ${data.min} - ${data.max}`
}

Типы

interface IRange {
  min: number;
  max: number;
}

type TComponentTypes = number | string | IRange;

type TData<T> = T

Обработка массива элементов управления

const params: ({ type: string, data: TData<TComponentTypes> })[] = [
  {
    type: "number",
    data: 15 ,
  },
  {
    type: "string",
    data: "data2 test" ,
  },
  {
    type: "range",
    data: { min: 0, max: 100 },
  },
]

params.forEach((param) => {
  switch (param.type) {
    case "number":
      console.log(component1(param.data as TData<number>))
      break
    case "string":
      console.log(component2(param.data as TData<string>))
      break
    case "range":
      console.log(component3(param.data as TData<IRange>))
      break
  }
})

На случай, если в настройках элементов управления что-то больше чем просто значения:


type TData1<T> = {
  name?: string,
  alt?: string,
  //...,
  value: T
}

const componentBig = (data: TData1<number>) => {
  return `${data.name || ""} ${data.alt || ""} ${data.value}`
}

const params1: ({ type: string, data: TData1<TComponentTypes> })[] = [
  {
    type: "number",
    data: { value: 15, name:"asdf" },
  },
  //...,
]
console.log(componentBig(params1[0].data as TData1<number>))



jsFiddle