Как использовать машинописный текст для установки уникального свойства?

У меня есть некоторые данные в моем коде, как показано ниже:

interface Product {
    id: number
    name: string;
}

enum EnumValue {
  'VALUE1' = 'VALUE1',
  'VALUE2' = 'VALUE2',
  'VALUE3' = 'VALUE3',
}

const data = {
  'VALUE1': {
    num1: {id: 1, name: '2'},
    num2: {id: 2, name: '2'},
  },
    'VALUE2': {
    num1: {id: 1, name: '2'},
  },
    'VALUE3': {
    num1: {id: 1, name: '2'},
  },
} as const satisfies { readonly [key in EnumValue]: { [key: string]: Product} };

Мне нужно определить валидатор для моего типа данных, чтобы для каждого свойства EnumValue использовались только уникальные идентификаторы. Я имею в виду

data = {
 'VALUE1': {
    num1: {id: 1, name: '2'},
    num2: {id: 1, name: '2'},
  },
    'VALUE2': {
    num1: {id: 1, name: '2'},
  },
    'VALUE3': {
    num1: {id: 1, name: '2'},
  },
}

ts должен выдать ошибку, поскольку VALUE1 имеет 2 объекта с идентификатором = 1, но

data = {
 'VALUE1': {
    num1: {id: 1, name: '2'},
    num2: {id: 2, name: '2'},
  },
    'VALUE2': {
    num1: {id: 1, name: '2'},
  },
    'VALUE3': {
    num1: {id: 1, name: '2'},
  },

является допустимым значением. мне нужна часть as const satisfies, чтобы использовать тип модели данных в моем коде. Итак, можете ли вы помочь мне определить валидатор для исправления моего типа данных?

Существует некоторый код для проверки уникальных идентификаторов массива объектов, который может помочь, но проблема в том, что я не знаю, как получить доступ к значениям объекта для повторения при проверке типа. ссылка на этот вопрос

interface IProduct<Id extends number> {
    id: Id
    name: string;
}

type Validation<
    Products extends IProduct<number>[],
    Accumulator extends IProduct<number>[] = []>
    =
    (Products extends []
        // #1 Last call
        ? Accumulator
        // #2 All calls but last
        : (Products extends [infer Head, ...infer Tail]
            ? (Head extends IProduct<number>
                // #3 Check whether [id] property already exists in our accumulator 
                ? (Head['id'] extends Accumulator[number]['id']
                    ? (Tail extends IProduct<number>[]
                        // #4 [id] property is a duplicate, hence we need to replace it with [never] in order to trigger the error
                        ? Validation<Tail, [...Accumulator, { id: never, name: Head['name'] }]>
                        : 1)
                    // #5 [id] is not a duplicate, hence we can add to our accumulator whole product
                    : (Tail extends IProduct<number>[]
                        ? Validation<Tail, [...Accumulator, Head]>
                        : 2)
                )
                : 3)
            : Products)
    )

Соответствует ли такой подход вашим потребностям? Если да, то я напишу ответ с объяснением; если нет, то что мне не хватает?

jcalz 24.02.2024 15:16

@jcalz Да, это решило мою проблему. Спасибо за ваше решение. Да, было бы неплохо, если бы вы тоже объяснили подход.

mhd sah 26.02.2024 06:55
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
1
2
169
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

В TypeScript нет конкретного типа ValidData, соответствующего вашему требованию, чтобы каждое свойство имело уникальные id подсвойства, поэтому вы не можете писать const data = {⋯} as const satisfies ValidData. Вместо этого вы можете создать универсальный ValidData<T> тип , который проверяет тип входных данных T и проверяет его вместе со вспомогательной validData() функцией. Итак, вы пишете const data = validData({⋯});, и в зависимости от введенных данных это либо получится, либо потерпит неудачу.


Сначала давайте напишем тип BasicData<K>, представляющий супертип всех типов ValidData<T>, где нас не волнует уникальность id; все, что нас волнует, это то, что у него есть ключи K и значения, все свойства которых являются Product:

type BasicData<K extends PropertyKey = EnumValue> =
  Record<K, { [k: string]: Product }>;

Обратите внимание, что я использовал аргумент общего типа по умолчанию, так что BasicData сам по себе соответствует BasicData<EnumValue>, что по сути то же самое, что вы использовали после satisfies.

Тогда ValidData<T> будет выглядеть так

type ValidData<T extends BasicData<keyof T>> =
  { [K in keyof T]: UniqueId<T[K]> }   

где UniqueId<T> — универсальный валидатор, который гарантирует, что T обладает уникальными id свойствами. Вот один из способов написать это:

type UniqueId<T extends Record<keyof T, Product>> =
  { [K in keyof T]: {
    id: Exclude<T[K]["id"], T[Exclude<keyof T, K>]["id"]>,
    name: string
  } }

Это сопоставленный тип над T, где каждое свойство является Product, свойство id которого явно исключает свойства id из всех остальных ключей. T[Exclude<keyof T, K>] — это объединение всех свойств T, кроме свойства с ключом K. И поэтому свойство id выглядит так: «Возьмите свойство id этого свойства и свойства Exclude всех остальных свойств».

Если свойства id уникальны, это ничего не будет исключать. Если это не так, то свойство id для дубликатов в конечном итоге будет типа «никогда». Итак, если id расширяет UniqueId<T>, то T допустимо. В противном случае T недействителен именно в тех свойствах, где есть повторяющиеся T.

Итак, теперь мы можем написать id вот так:

const validData =
  <const T extends BasicData & BasicData<keyof T> & ValidData<T>>(
    d: T) => d;

Здесь используется параметр типа const , поэтому вызывающим абонентам не нужно помнить об использовании утверждения const . Ограничение — это пересечение всех отдельных ограничений, которые нас интересуют. Первый — validData(), что означает, что он должен иметь все ключи BasicData. Второй — EnumValue, что означает, что для каждого свойства, которое оно имеет, все свойства должны быть набором BasicData<keyof T>. И последний — Product, что означает, что его свойства должны иметь уникальные ValidData<T>.


Хорошо, давайте проверим:

const data = validData({
  'VALUE1': {
    num1: { id: 1, name: '2' },
    num2: { id: 2, name: '2' },
  },
  'VALUE2': {
    num1: { id: 1, name: '2' },
  },
  'VALUE3': {
    num1: { id: 1, name: '2' },
  },
}); // okay

const data2 = validData({
  'VALUE1': {
    num1: { id: 1, name: '2' }, // error
    num2: { id: 1, name: '2' }, // error
  },
  'VALUE2': {
    num1: { id: 1, name: '2' },
  },
  'VALUE3': {
    num1: { id: 1, name: '2' },
  },
});

Выглядит неплохо. Ибо id всё проходит. Для data1 свойство data2 не работает, а свойства 'VALUE1' как для id, так и для num1 содержат ошибки, говорящие о том, что они не могут быть назначены num2. Это не самое красивое сообщение об ошибке, но оно работает.

Вы можете сделать что-нибудь сложное, чтобы сделать сообщение об ошибке более понятным:

type Repl<T, U> = [T] extends [never] ? U : T;

// https://github.com/microsoft/TypeScript/issues/23689 workaround
interface ErrMsg<T> { nope: never, msg: T } 

type UniqueId<T extends Record<keyof T, Product>> =
  { [K in keyof T]: {
    id: Repl<Exclude<T[K]["id"], T[Exclude<keyof T, K>]["id"]>,
      ErrMsg<`conflicts with ${string &
        { [P in keyof T]:
          T[K]["id"] & T[P]["id"] extends never ? never : Exclude<P, K>
        }[keyof T]}`>>,
    name: string
  } }


const data2 = validData({
  'VALUE1': {
    num1: { id: 1, name: '2' }, // ErrMsg<"conflicts with num2">
    num2: { id: 1, name: '2' }, // ErrMsg<"conflicts with num1">
  },
  'VALUE2': {
    num1: { id: 1, name: '2' },
  },
  'VALUE3': {
    num1: { id: 1, name: '2' },
  },
});

Но я считаю, что это выходит за рамки данного вопроса, и не буду отвлекаться дальше, объясняя это здесь.

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

Другие вопросы по теме