Ограничение типа для расширения объекта в TypeScript не работает — строка принимается

Я пытаюсь обеспечить типизацию объекта attributes, содержащегося в классе-оболочке. Причиной этого является необходимость правильного ввода текста при установке или получении отдельных атрибутов (методы getOne/setOne в коде ниже).

Однако, похоже, это работает неправильно — строка с радостью принимается в качестве параметра конструктора AttributeCollection, что не соответствует назначению класса:

type AttributeMap<M extends object> = {
  [key in keyof M]: M[key];
};

export class AttributeCollection<M extends object> {
  constructor(private attributes: AttributeMap<M>) {}

  public getOne<K extends keyof AttributeMap<M>>(key: K): M[K] {
    return this.attributes[key];
  }

  public setOne<K extends keyof AttributeMap<M>>(key: K, value: M[K]): void {
    this.attributes[key] = value;
  }
}

const acCorrectImplicit = new AttributeCollection({ a: 'str'}); // ok
const acCorrectExplicit = new AttributeCollection<{ a: string }>({ a: 'str'}); // ok

const acWrong = new AttributeCollection('str'); // ok, but shouldn't be

/*

The typing we get:

const acWrong: AttributeCollection<{
    toString: () => string;
    charAt: (pos: number) => string;
    charCodeAt: (index: number) => number;
    concat: (...strings: string[]) => string;
    indexOf: (searchString: string, position?: number | undefined) => number;
    ... 37 more ...;
    [Symbol.iterator]: () => IterableIterator<...>;
}>

*/

const am: AttributeMap<'str'> = 'str'; // error

console.info(acCorrectImplicit, acCorrectExplicit, acWrong, am);

Вероятно, я где-то неправильно передаю типы. Можете ли вы помочь мне понять, в чем дело?

Вот детская площадка.

• Какой здесь смысл AttributeMap? Это выглядит как пустая операция, и вместо этого вы просто должны написать `constructor(private Attributes: M){}`. Какой вариант использования мотивирует это? • Похоже, что string по существу расширяется до String (или эквивалента), который не рассматривается как примитив. Вы можете перейти на эту версию, если хотите гарантировать, что выходные данные также не будут примитивными. Но ищете ли вы обходной путь или объяснение неожиданного поведения? (а если «оба», то что важнее?)

jcalz 22.05.2024 18:12

@jcalz, я фактически удалил методы, в которых использовал этот тип для краткости. Я хотел иметь возможность получать/устанавливать отдельные атрибуты следующим образом: public setOne<K extends keyof AttributeMap<M>>(key: K, value: M[K]): void { this.attributes[key] = value; }. По сути, это было основное намерение. И в качестве бонуса хотелось понять, что здесь происходит. Вот код: tsplay.dev/mA0q1N

Ivan Yarych 22.05.2024 18:22

А можно просто написать public setOne<K extends keyof M>(key: K, value: M[K]): void { this.attributes[key] = value; }. Кажется, в AttributeMap нет никакого смысла. Если его определение действительно просто {[K in keyof T]: T[K]}, то это причудливый способ записи T для большинства целей. Итак, еще раз, какой вариант использования мотивирует это?

jcalz 22.05.2024 18:23

Вы правы @jcalz — я обновлю свой вопрос, чтобы было понятнее, чего я хотел достичь, и вы, возможно, дадите ответ, который я приму. Я пытался сделать типизацию правильной для отдельных атрибутов, и этот дополнительный тип, похоже, справился с этой задачей. Но ваше предложение будет работать намного лучше и будет более понятным.

Ivan Yarych 22.05.2024 18:35
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой Zod и раскрыть некоторые ее особенности, например, возможности валидации и трансформации данных, а также...
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Мне нравится библиотека Mantine Component , но заставить ее работать без проблем с Remix бывает непросто.
Угловой продивер
Угловой продивер
Оригинал этой статьи на турецком языке. ChatGPT используется только для перевода на английский язык.
TypeScript против JavaScript
TypeScript против JavaScript
TypeScript vs JavaScript - в чем различия и какой из них выбрать?
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Не все нужно хранить на стороне сервера. Иногда все, что вам нужно, это постоянное хранилище на стороне клиента для хранения уникальных для клиента...
Что такое ленивая загрузка в Angular и как ее применять
Что такое ленивая загрузка в Angular и как ее применять
Ленивая загрузка - это техника, используемая в Angular для повышения производительности приложения путем загрузки модулей только тогда, когда они...
3
4
54
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Решение:

Используйте NoInfer<M>, чтобы запретить TS выводить примитивные методы как объект:

export class AttributeCollection<M extends object> {
  constructor(private attributes: AttributeMap<NoInfer<M>>) {}

  public getAttributes(): AttributeMap<NoInfer<M>> {
    return this.attributes;
  }
}

Примечание. Для этого требуется typescript@^5.4.

const acWrong = new AttributeCollection("string"); // error as expected

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

Это похоже на ошибку в TypeScript, которая считает примитивные методы объектом, а не фактическим типом.

Я бы не стал этого делать; это полностью блокирует вывод. acCorrectImplicit, например, больше не тот тип.

jcalz 22.05.2024 18:13

AttributeMap чувствует себя лишним. Я чувствую, что вам нужен более простой тип.

export class AttributeCollection<M extends Record<string, unknown>> {
  constructor(private attributes: M) {}

  public getOne<K extends keyof M>(key: K): M[K] {
    return this.attributes[key];
  }

  public setOne<K extends keyof M>(key: K, value: M[K]): void {
    this.attributes[key] = value;
  }
}

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

Похоже, что сопоставленный тип AttributeMap<M> приводит к расширению аргумента выведенного типа от string (примитив) до интерфейса String (тип объекта-оболочки), и он не отклоняется. Это может быть или не быть ошибкой в ​​TypeScript; Мне не удалось легко найти существующую проблему.

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

type AttributeMap<M extends object> = object & {
  [K in keyof M]: M[K];
};

Тогда все работает как хотелось:

const acCorrectImplicit = new AttributeCollection({ a: 'str' }); // ok
const acCorrectExplicit = new AttributeCollection<{ a: string }>({ a: 'str' }); // ok
const acWrong = new AttributeCollection("str"); // error

Конечно, в приведенном примере вообще нет очевидной причины использовать AttributeMap. По сути, это функция идентификации типов. Итак, AttributeMap<M> более или менее эквивалентно M. А если вы просто используете M напрямую, то ваша проблема тоже уходит:

export class AttributeCollection<M extends object> {
  constructor(private attributes: M) { }

  public getAttributes(): M {
    return this.attributes;
  }
}

const acCorrectImplicit = new AttributeCollection({ a: 'str' }); // ok
const acCorrectExplicit = new AttributeCollection<{ a: string }>({ a: 'str' }); // ok
const acWrong = new AttributeCollection("str"); // error

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

Я обновил код, включив в него методы getOne/setOne, используемые для управления отдельными атрибутами, и они отлично работают, когда я заменяю AttributeMap<M> просто на M.

Ivan Yarych 22.05.2024 23:37

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

Похожие вопросы

Машинописный текст: переменная используется до назначения – не так ли?
Машинопись: RXJS `distinctUntilChanged` не работает со строгим/первым параметром неопределенным
Машинописный текст: необязательный оператор/путаница с нулем
PrismaClient не может работать в этой среде браузера или был включен в состав браузера (работает в «неизвестном») при обновлении
Невозможно найти модуль @google/generative-ai/files с наличием файла объявления типа (ts(2307))
Как изолировать вызов API для одного компонента в этом приложении Angular 16?
Ожидаемый тип берется из свойства, которое объявлено здесь для типа IntrinsicAttributes & --prop
Принудительно установить для параметра универсального типа значение T или значение NULL
Как найти ключи значений в json с помощью forEach и изменить значения соответствующих ключей с помощью машинописного текста vue js?
Общий TypeScript для управления данными Supabase