Литерал шаблона из keyof в классе

Я пытаюсь объединить сразу несколько функций TypeScript.

Я создал класс, который инициализируется объектом и имеет набор функций, которые должны иметь возможность принимать в качестве аргументов только ключи этого объекта. Все идет нормально. Помогли с этим вчера, и моя базовая установка выглядит так:

class MyClass<Type> {
  properties: Partial<Type> = {};

  constructor(properties: Type) {
    this.properties = properties;
  }
  
  pick(...propNames: Array<keyof Type>) {
    return propNames.reduce((obj, name) => ({ 
      ...obj, 
      [name]: this.properties[name]
    }), {} as Partial<Type>);
  }
}

Но на самом деле я хочу, чтобы оба были приняты сами ключи и слегка измененные версии этих ключей, в частности, как "key", так и "key!". В моем случае, если вы добавите восклицательный знак к ключу, этот (допустимый) ввод используется для указания на то, что ключ требуется.

Я нашел шаблонные литералы в справочнике и увидел, что могу сделать что-то вроде этого:

type Name = 'John';
type BangName = `${Name}!`;

Идеальный! Однако я понятия не имею, как интегрировать это в мой вариант использования, в котором первый тип генерируется из keyof, и эта интеграция происходит внутри класса JS.

Я видел этот вопрос, который приближается. Это предполагает, что я мог бы сделать что-то вроде приведенного ниже, который генерирует комбинированный интерфейс, из которого я могу получить приемлемые ключи, используя keyof:

type BangType = {
  [Key in keyof Type as `${Key}!`]: Type[Key];
}

type CombinedType = Type & BangType;

а потом:

pick(...propNames: Array<keyof CombinedType>) {

Но я не знаю, куда я могу сгенерировать этот CombinedType, так как общий Type определен в сигнатуре класса. Это возможно?

Ниже, похоже, не работает:

pick(...propNames: Array<keyof PropType> | Array<Key in keyof PropType as `${Key}!`>)

Соответствует ли этот подход вашим потребностям? Я не совсем понимаю, как ваша реализация будет справляться с "!", поэтому я собрал кое-что вместе; если это не актуально, то, возможно, удалите реализацию из вашего примера кода? В любом случае, если это сработает для вас, я мог бы написать ответ; если нет, то что мне не хватает?

jcalz 16.05.2022 21:21

Это редко имеет эффект, когда я говорю это, но отображаемые типы перебирает параметры ключевого типа, а не ключевые имена, поэтому они обычно начинаются с заглавной буквы, как и любой другой параметр типа; поэтому {[k in XXX]: YYY} нетрадиционный, а {[K in XXX]: YYY} обычный. Пожалуйста, рассмотрите возможность замены key in keyof ... на Key in keyof ... или K in keyof .... }

jcalz 16.05.2022 21:25

Благодарю вас! Работал как шарм, и я буду следовать условности. Если вы хотите сделать это официальным ответом на баллы, я бы с удовольствием принял его.

Sasha 16.05.2022 23:36
В чем разница между Promise и Observable?
В чем разница между Promise и Observable?
Разберитесь в этом вопросе, и вы значительно повысите уровень своей компетенции.
Создание собственной системы электронной коммерции на базе Keystone.js - настройка среды и базовые модели
Создание собственной системы электронной коммерции на базе Keystone.js - настройка среды и базовые модели
Прошлая статья была первой из цикла статей о создании системы электронной коммерции с использованием Keystone.js, и она была посвящена главным образом...
1
3
26
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Здесь вы действительно можете использовать литеральные типы шаблонов. Если вы хотите, чтобы propNames был массивом либо keyof T, либо «строк, которые вы получите, если добавите "!" в конец строк в keyof T», то тип, который вы ищете,

Array<keyof T | `${Extract<keyof T, string>}!`>

Обратите внимание, что, поскольку T — это некоторый тип общий, которым вы не управляете, у него могут быть ключи со значением symbol. А так как keyof T может быть symbol в нем, компилятор будет недоволен, если вы попытаетесь просто написать шаблонный литеральный тип `${keyof T}!` напрямую, потому что шаблонные литеральные типы могут сериализовать только strings и numbers (э-э, и bigints, и booleans, и null и undefined. Но это так):

oops(...propNames: Array<keyof T | `${keyof T}!`>) { } // error!
// ---------------------------------> ~~~~~~~
// 'symbol' is not assignable to 'string | number | bigint | boolean | null | undefined'.

Поскольку нас интересуют только string-значные ключи T, мы используем тип утилиты Extract<T, U>, чтобы получить только их. И тут компилятор не жалуется:

pick(...propNames: Array<keyof T | `${Extract<keyof T, string>}!`>) {
  return propNames.map(k =>
    typeof k === "string" ? k.replace(/!$/, "") as keyof T : k
  ).reduce((obj, name) => ({
    ...obj,
    [name]: this.properties[name]
  }), {} as Partial<T>);
}

И давайте убедимся, что это работает:

const n = new MyClass({ a: 1, b: "two", c: true });    
const x = n.pick("b!");
console.log(x) // {b: "two"}

Выглядит неплохо!

Ссылка на код для игровой площадки

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