Как типизировать объект, у которого 1 ключ вариативен?

с бэка приходит массив объектов с такой структурой

export interface IUserNotifySubscribesResponse {
    id: number
    mId: number
    uId: number
    notifyType: UserNotifyType
    notifyRulesJson: string
  }


  export enum UserNotifyType {
    UNOTIFY_BY_MODEL,
    UNOTIFY_BY_WEBCASE,
    UNOTIFY_BY_PLATFORM,
  }

для работе на фронте , создается новый тип в котором notifyRulesJson парсится

  export interface IUserNotifySubscribes extends IUserNotifySubscribesResponse {
    notifyRulesObj: RulesObj
  }

RulesObj в свою очередь вариативен

 export type RulesObj = SubscriptionSettings | CasesSettings | PlatformSettings

  export interface SubscriptionSettings {
    modelNotify: boolean
    defect: boolean
  }

  export interface PlatformSettings {
    platform: boolean
    sms: boolean
    email: boolean
  }
  
  export interface CasesSettings {
    cases: boolean
  }


как сделать, чтобы TypeScript понимал, что именно приходит в RulesObj

Полагаю тут нужны дженерики,

export interface IUserNotifySubscribes<T extends RulesObj> extends IUserNotifySubscribesResponse {
  notifyRulesObj: T
}

но в таком случае не совсем понимаю как нужно типизировать массив, ведь в нем объекты всех 3х вариаций


Ответы (3 шт):

Автор решения: MoloF

В вашем случае не нужны дженерики, воспользуйтесь защитой типа (Type Guards)

Создайте интерфейс с перечислением всех реализаций:

export interface SubscriptionSettings {
  modelNotify: boolean
  defect: boolean
}

export interface PlatformSettings {
  platform: boolean
  sms: boolean
  email: boolean
}

export interface CasesSettings {
  cases: boolean
}

export type RulesObj = SubscriptionSettings | CasesSettings | PlatformSettings

export interface UserNotifySubscribes extends UserNotifySubscribesResponse {
  notifyRulesObj: RulesObj
}

Затем создайте функции которые будут проверять ключи или любые другие данные с указанным типом.

export function isUserNotifySubscriptionSettings(value: RulesObj): value is SubscriptionSettings {
  return 'modelNotify' in value;
}
export function isUserNotifyPlatformSettings(value: RulesObj): value is PlatformSettings {
  return 'platform' in value;
}
export function isUserNotifyCasesSettings(value: RulesObj): value is SubscriptionSettings {
  return 'cases' in value;
}

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

export async function main() {
  const result: UserNotifySubscribes = await getUserNotifySubscribes();

  if (isUserNotifySubscriptionSettings(result.notifyRulesObj)) {
    console.log(result.notifyRulesObj.modelNotify);
  } else if (isUserNotifyPlatformSettings(result.notifyRulesObj)) {
    console.log(result.notifyRulesObj.platform);
  } else if (isUserNotifyCasesSettings(result.notifyRulesObj)) {
    console.log(result.notifyRulesObj.cases);
  }

  // Another logic
}

Не могу подсказать более элегантного и type safety решения. Уверен есть и другие решения которые не нарушают какие-то принципы.

Весь код тут:

export interface UserNotifySubscribesResponse {
  id: number
  mId: number
  uId: number
  notifyType: NotifyType
  notifyRulesJson: string
}

export enum NotifyType {
  UNOTIFY_BY_MODEL,
  UNOTIFY_BY_WEBCASE,
  UNOTIFY_BY_PLATFORM,
}

export interface SubscriptionSettings {
  modelNotify: boolean
  defect: boolean
}

export interface PlatformSettings {
  platform: boolean
  sms: boolean
  email: boolean
}

export interface CasesSettings {
  cases: boolean
}

export type RulesObj = SubscriptionSettings | CasesSettings | PlatformSettings

export interface UserNotifySubscribes extends UserNotifySubscribesResponse {
  notifyRulesObj: RulesObj
}

export async function getUserNotifySubscribes(): Promise < UserNotifySubscribes > {
  const response: UserNotifySubscribesResponse = {
    id: 0,
    mId: 0,
    uId: 0,
    notifyType: NotifyType.UNOTIFY_BY_WEBCASE,
    notifyRulesJson: '',
  }

  const result = {}
  as UserNotifySubscribes;
  Object.assign(result, response, {
    notifyRulesObj: {
      cases: false,
    }
  });
  return result;
}

export function isUserNotifySubscriptionSettings(value: RulesObj): value is SubscriptionSettings {
  return 'modelNotify' in value;
}
export function isUserNotifyPlatformSettings(value: RulesObj): value is PlatformSettings {
  return 'platform' in value;
}
export function isUserNotifyCasesSettings(value: RulesObj): value is SubscriptionSettings {
  return 'cases' in value;
}

export async function main() {
  const result: UserNotifySubscribes = await getUserNotifySubscribes();

  if (isUserNotifySubscriptionSettings(result.notifyRulesObj)) {
    console.log(result.notifyRulesObj.modelNotify);
  } else if (isUserNotifyPlatformSettings(result.notifyRulesObj)) {
    console.log(result.notifyRulesObj.platform);
  } else if (isUserNotifyCasesSettings(result.notifyRulesObj)) {
    console.log(result.notifyRulesObj.cases);
  }

  // Another logic
}

→ Ссылка
Автор решения: Qwertiy
type IUserNotifySubscribes = {
  [key in UserNotifyType]: IUserNotifySubscribesResponse & {
    notifyType: key,
    notifyRulesObj: {
      [UserNotifyType.UNOTIFY_BY_MODEL]: SubscriptionSettings,
      [UserNotifyType.UNOTIFY_BY_WEBCASE]: CasesSettings,
      [UserNotifyType.UNOTIFY_BY_PLATFORM]: PlatformSettings,
    }[key]
  }
}[UserNotifyType]

Код полностью: playground

export interface IUserNotifySubscribesResponse {
  id: number
  mId: number
  uId: number
  notifyType: UserNotifyType
  notifyRulesJson: string
}

export enum UserNotifyType {
  UNOTIFY_BY_MODEL,
  UNOTIFY_BY_WEBCASE,
  UNOTIFY_BY_PLATFORM,
}

export type RulesObj = SubscriptionSettings | CasesSettings | PlatformSettings

export interface SubscriptionSettings {
  modelNotify: boolean
  defect: boolean
}

export interface PlatformSettings {
  platform: boolean
  sms: boolean
  email: boolean
}

export interface CasesSettings {
  cases: boolean
}

type IUserNotifySubscribes = {
  [key in UserNotifyType]: IUserNotifySubscribesResponse & {
    notifyType: key,
    notifyRulesObj: {
      [UserNotifyType.UNOTIFY_BY_MODEL]: SubscriptionSettings,
      [UserNotifyType.UNOTIFY_BY_WEBCASE]: CasesSettings,
      [UserNotifyType.UNOTIFY_BY_PLATFORM]: PlatformSettings,
    }[key]
  }
}[UserNotifyType]

declare const data: IUserNotifySubscribes[]

for (const x of data) {
  switch (x.notifyType) {
    case UserNotifyType.UNOTIFY_BY_PLATFORM:
      console.log(x.notifyRulesObj.platform)
      break

    case UserNotifyType.UNOTIFY_BY_MODEL:
      console.log(x.notifyRulesObj.defect)
      break
    
    case UserNotifyType.UNOTIFY_BY_WEBCASE:
      console.log(x.notifyRulesObj.cases)
      break
  }
}
→ Ссылка
Автор решения: Andrei Khotko

Можно также использовать Union Types

type IUserNotifySubscribes = IUserNotifySubscribesResponse & ({
  notifyType: UserNotifyType.UNOTIFY_BY_MODEL;
  notifyRulesObj: SubscriptionSettings;
} | {
  notifyType: UserNotifyType.UNOTIFY_BY_WEBCASE;
  notifyRulesObj: CasesSettings;
} | {
  notifyType: UserNotifyType.UNOTIFY_BY_PLATFORM;
  notifyRulesObj: PlatformSettings;
});

Полное решение:

TS Playground

export interface IUserNotifySubscribesResponse {
  id: number
  mId: number
  uId: number
  notifyType: UserNotifyType
  notifyRulesJson: string
}

export enum UserNotifyType {
  UNOTIFY_BY_MODEL,
  UNOTIFY_BY_WEBCASE,
  UNOTIFY_BY_PLATFORM,
}

export interface SubscriptionSettings {
  modelNotify: boolean
  defect: boolean
}

export interface PlatformSettings {
  platform: boolean
  sms: boolean
  email: boolean
}

export interface CasesSettings {
  cases: boolean
}

type IUserNotifySubscribes = IUserNotifySubscribesResponse & ({
  notifyType: UserNotifyType.UNOTIFY_BY_MODEL;
  notifyRulesObj: SubscriptionSettings;
} | {
  notifyType: UserNotifyType.UNOTIFY_BY_WEBCASE;
  notifyRulesObj: CasesSettings;
} | {
  notifyType: UserNotifyType.UNOTIFY_BY_PLATFORM;
  notifyRulesObj: PlatformSettings;
});

const userNotifySubscribes: IUserNotifySubscribes[] = []; // Получение данных с сервера

// Можно сужать тип через switch case
userNotifySubscribes.forEach((item) => {
  switch (item.notifyType) {
    case UserNotifyType.UNOTIFY_BY_MODEL:
      item.notifyRulesObj; // SubscriptionSettings
      break;
    case UserNotifyType.UNOTIFY_BY_WEBCASE:
      item.notifyRulesObj; // CasesSettings
      break;    
    case UserNotifyType.UNOTIFY_BY_PLATFORM:
      item.notifyRulesObj; // PlatformSettings
      break;
  }
});

// Можно сужать тип через if
userNotifySubscribes.forEach((item) => {
  if (item.notifyType === UserNotifyType.UNOTIFY_BY_MODEL) {
    item.notifyRulesObj; // SubscriptionSettings
  } else if (item.notifyType === UserNotifyType.UNOTIFY_BY_WEBCASE) {
    item.notifyRulesObj; // CasesSettings
  } else if (item.notifyType === UserNotifyType.UNOTIFY_BY_PLATFORM) {
    item.notifyRulesObj; // PlatformSettings
  }
});
→ Ссылка