Использование дженериков для сопоставления одного строкового литерала в Typescript

Класс MyClass взаимодействует с подсистемой индексирования, которая требует, чтобы каждая запись (Record<string, any>) содержала определенное поле первичного ключа. Я пытаюсь использовать систему типов Typescript, чтобы гарантировать, что каждая запись, переданная MyClass, содержит это поле первичного ключа, задав ему строковый литерал в качестве общего параметра. Мне не нужно использовать значение поля; только для того, чтобы убедиться, что он существует в каждой записи. Можно ли создать класс MyClass<T...> так, чтобы T... соответствовал только строковому литералу?

Пример использования MyClass<T>:

class MyClass<T ...> { // Can T match a single string literal?
  add(arg: Record<T, any> & Record<string, any>): void {}
  get(arg: T): Record<T, any> & Record<string, any> {}
};

const myClass = new MyClass<"requiredKey">();
myClass.add({
    requiredKey: "someValue";
    //...
}); // Ok
myClass.add({
    someOtherKey: "someValue";
}); // Rejected by the compiler

Я начал определять тип, который, как мне казалось, был ограничен строковым литералом, а не объединением, основываясь на предыдущем ответе

type UnionToIntersection<U> =
  (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
type StringLiteral<T extends string> = [T] extends [UnionToIntersection<T>]
  ? string extends T ? never : string : never;

Мои тестовые примеры:

class MyClass<K extends StringLiteral<K>> {
  constructor() {}
  add(arg: Record<K, any> & Record<string, any>) {}
  get(arg: Record<K, any>): Record<K, any> & Record<string, any> { return null!; }
};

let t = new MyClass<"a">();        // Ok: Valid
let u = new MyClass<"a" | "b">();  // Ok: Invalid `"a" | "b"` does not satisfy `never`
let v = new MyClass<string>();     // Ok: Invalid `string` does not satisfy `never`
let w = new MyClass<1>();          // Ok: Invalid `1` does not extend `string`
let x = new MyClass<`${"a"}`>();   // Ok: String template literals are fine
let z = new MyClass<never>();      // Not ok, but I can live with that...

Я доволен таким подходом; но поскольку я новичок в Typescript, мне интересно, есть ли более простое решение.

В настоящее время использую машинопись 5.5.

ДЛИННОЕ РЕДАКТИРОВАНИЕ После обсуждения в комментариях я перефразировал вопрос, чтобы лучше проиллюстрировать проблему. Оставьте комментарий, если все еще неясно.

Каков основной вариант использования? Что плохого произойдет, если у arg более одного ключа? Потому что что бы ты ни делал в ТС, такое всё равно может случиться . Попытка заставить TS не допускать дополнительные свойства — это, по большому счету, напрасная трата усилий. Пожалуйста, отредактируйте, чтобы уточнить вариант использования; вполне возможно, что есть подход, который лучше подходит для этого.

jcalz 21.07.2024 15:01

(см. предыдущий комментарий). Также обратите внимание, что TS допускает подписи индекса шаблона строки шаблона , поэтому вы можете захотеть защититься от того, чтобы K было чем-то вроде `abc${number}def`, как показано в этой ссылке на игровую площадку . Вас это волнует? В любом случае, пожалуйста отредактируйте, чтобы внести ясность.

jcalz 21.07.2024 16:43

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

sbgrl 22.07.2024 06:43

Я понял, что могу упростить шаблонное выражение в MyClass и добиться того же эффекта. Поэтому я изменил class MyClass<K extends [K] extends [StringLiteral<K>] ? string : never> на class MyClass<K extends StringLiteral<K>>, что имеет тот же смысл, если я также изменю StringLiteral<K>, чтобы вернуть string вместо any.

sbgrl 22.07.2024 07:10

• Почему конструктор не принимает имя поля в качестве ключа? Я не понимаю, как требование, чтобы K было чем-то конкретным, имеет значение для кода здесь. Мне бы хотелось увидеть, как произойдет что-то плохое, если вы позволите K быть, скажем, "a" | "b". • Нет, я говорю не о `${"a"}` (это просто "a"). Я говорю о шаблонных строковых шаблонах, таких как `abc${number}def`. Вы видите это number там? Это соответствует нескольким строкам, таким как "abc1def" и "abc5632def". Значит, это уже не один ключ, и ваша StringLiteralутилита неправильно его классифицирует. Вы понимаете?

jcalz 22.07.2024 14:03

• Поскольку я поместил проверки в функции-члены, чтобы убедиться, что записи содержат ключ, я решил изучить, можно ли использовать систему типов, чтобы избежать необходимости в проверках. • Спасибо за дополнительную точность, теперь я понимаю, что вы имеете в виду, и определенно узнал здесь что-то новое о Typescript. StringLiteral<K> не будет ограничивать K одним строковым литералом, как вы изначально сказали, это была напрасная трата усилий. Более простой MyClass<K extends string> дал бы аналогичный результат.

sbgrl 23.07.2024 05:03

Я думаю, что вам потребуются проверки во время выполнения, несмотря ни на что, но я не вижу этих проверок в вашем коде, поэтому я не уверен, как вы вообще определяете, какой ключ вы ищете (здесь код просто имеет ключ как тип , поэтому среда выполнения не знает об этом.) Вы можете попытаться ограничиться одним строковым литералом, но это сложно и, вероятно, хрупко. ... Так что же вы хотите здесь увидеть? Ответ «не пытайтесь это сделать»? Ответ, показывающий, насколько мы можем приблизиться к обеспечению одного строкового литерала?

jcalz 23.07.2024 14:31

Подробно о сценарии использования: MyClass передает запись внешней подсистеме, которая выполняет индексацию; Сначала я написал методы MyClass с проверками, такими как Object.keys(arg).includes(this.primaryKey);, прежде чем использовать вместо них типы. Мне интересно понять, как работает ваша игровая площадка, с оговоркой, что может быть полезно только убедиться в наличии указанного ключа.

sbgrl 24.07.2024 05:56

Посетите stackoverflow.com/questions/60872063/…

JasonB 24.07.2024 06:32
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой 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 для повышения производительности приложения путем загрузки модулей только тогда, когда они...
1
9
69
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Боюсь, я до сих пор не вижу варианта использования, который требует, чтобы тип был одной строкой литеральным типом, но в дальнейшем я посмотрю, насколько близко мы можем подойти.


Во-первых, мы можем написать служебный тип IsUnion<T, Y, N>, чтобы определить, является ли тип ввода Tобъединением, и вернуть Y, если да, и N, если нет:

type IsUnion<T, Y = unknown, N = never, U = T> =
    T extends unknown ? [U] extends [T] ? N : Y : never

Это распределительный условный тип в T. Он разбивает T на члены союза и проверяет каждого члена на соответствие всему союзу U (который использует аргумент типа по умолчанию, чтобы быть нераспространённой копией T). Если каждому члену можно назначить весь союз, это означает, что есть только один член, и мы возвращаем N. В противном случае мы возвращаемся Y.


Затем мы можем написать служебный тип StrLit<T, Y, N>, чтобы определить, является ли тип входной строки (предположительно не объединенный) T строковым литералом, или это что-то более широкое, например string или шаблонный литерал (как реализовано в microsoft/TypeScript#40498), например `foo${string}`:

type StrLit<T extends string, Y = unknown, N = never> =
    T extends `${infer F}${infer R}` ? (
        ["0", "1"] extends [F, F] ? N : StrLit<R, Y, N>
    ) : T extends "" ? Y : N

На самом деле не существует чистого способа сделать это. Единственный способ, который я нашел, — это использовать литералы шаблона для анализа одного «символа» за раз. Если какой-либо символ является чем-то широким, то это не строковый литерал. Под «чем-то широким» я подразумеваю либо T, либо один из других возможных типов литералов шаблона, например string или `${number}`. Мой тест таков: если ему можно назначить оба `${bigint}` и "0", то он широкий. (Обратите внимание, что "1" и "0" являются допустимыми строками, действительными сериализованными числами и действительными сериализованными "1"). Когда мы закончим анализ, мы должны получить bigint, что означает, что мы нашли строковый литерал. Если нет (поскольку последний «символ» строкового типа широкий), то это не так.


И теперь мы можем написать тип проверки утилиты "", который вычисляет SingleKey<T>, если unknown (предположительно тип объекта) имеет один строковый ключ-литерал, и T в противном случае:

type SingleKey<T> = IsUnion<keyof T, never,
    keyof T extends string ? StrLit<keyof T> : never>

class MyClass<T extends object & SingleKey<T>> {
  constructor() { }
  functionThatMeetsMyGoal(arg: T) { return arg }
}

Давайте проверим это:

new MyClass<{key: any}>(); // okay   
new MyClass<{key1: any; key2: any}>(); // error
new MyClass<{ [k: `abc${number}def`]: string }>() // error
new MyClass<{ [k: number]: string }>() // error

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


Вот и все. Я бы не рекомендовал пытаться использовать это, чтобы гарантировать наличие отдельных ключей в типе.

Прежде всего, ничто не мешает значению такого типа иметь более одного ключа, поскольку TypeScript не имеет «точных типов», как описано в microsoft/TypeScript#. Вы всегда можете расширить типы с дополнительными свойствами до типов без них:

let x = { key: "abc", oopsie: 123 };
let y: { key: any } = x; // this is allowed

Таким образом, такие типы не очень полезны для предотвращения того, чтобы значения времени выполнения имели больше свойств, чем вы ожидаете.

И что еще более важно, вышеупомянутые типы утилит сложны и, вероятно, хрупки. TypeScript на самом деле не предполагает, чтобы вы могли запрашивать «является ли это одиночным строковым литералом», и приведенная выше реализация зависит от точного знания того, какие подтипы never TypeScript поддерживает, и использования уловок для их различения. В будущих версиях TypeScript могут быть добавлены или изменены типы строк таким образом, чтобы нарушить это правило. Поэтому я бы держался подальше от чего-либо подобного в любой производственной системе, которая от этого зависит. Это интересно исследовать, но не стоит полагаться на это.

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

Спасибо @jcalz, это было очень поучительно!

sbgrl 25.07.2024 05:34

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