Я пытаюсь обеспечить типизацию объекта 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);
Вероятно, я где-то неправильно передаю типы. Можете ли вы помочь мне понять, в чем дело?
Вот детская площадка.
@jcalz, я фактически удалил методы, в которых использовал этот тип для краткости. Я хотел иметь возможность получать/устанавливать отдельные атрибуты следующим образом: public setOne<K extends keyof AttributeMap<M>>(key: K, value: M[K]): void { this.attributes[key] = value; }. По сути, это было основное намерение. И в качестве бонуса хотелось понять, что здесь происходит. Вот код: tsplay.dev/mA0q1N
А можно просто написать 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 — я обновлю свой вопрос, чтобы было понятнее, чего я хотел достичь, и вы, возможно, дадите ответ, который я приму. Я пытался сделать типизацию правильной для отдельных атрибутов, и этот дополнительный тип, похоже, справился с этой задачей. Но ваше предложение будет работать намного лучше и будет более понятным.






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