Класс 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.
ДЛИННОЕ РЕДАКТИРОВАНИЕ После обсуждения в комментариях я перефразировал вопрос, чтобы лучше проиллюстрировать проблему. Оставьте комментарий, если все еще неясно.
(см. предыдущий комментарий). Также обратите внимание, что TS допускает подписи индекса шаблона строки шаблона , поэтому вы можете захотеть защититься от того, чтобы K
было чем-то вроде `abc${number}def`
, как показано в этой ссылке на игровую площадку . Вас это волнует? В любом случае, пожалуйста отредактируйте, чтобы внести ясность.
@jcalz Я обновил первый абзац и часть примера кода, чтобы, надеюсь, дать хорошее представление о моем варианте использования. Шаблоны строк шаблонов по-прежнему подходят для моего варианта использования, если пользователь определит их, как показано в вашем примере. Я не рассматривал символы, и мне нужно прочитать об этом больше.
Я понял, что могу упростить шаблонное выражение в MyClass
и добиться того же эффекта. Поэтому я изменил class MyClass<K extends [K] extends [StringLiteral<K>] ? string : never>
на class MyClass<K extends StringLiteral<K>>
, что имеет тот же смысл, если я также изменю StringLiteral<K>
, чтобы вернуть string
вместо any
.
• Почему конструктор не принимает имя поля в качестве ключа? Я не понимаю, как требование, чтобы K
было чем-то конкретным, имеет значение для кода здесь. Мне бы хотелось увидеть, как произойдет что-то плохое, если вы позволите K
быть, скажем, "a" | "b"
. • Нет, я говорю не о `${"a"}`
(это просто "a"
). Я говорю о шаблонных строковых шаблонах, таких как `abc${number}def`
. Вы видите это number
там? Это соответствует нескольким строкам, таким как "abc1def"
и "abc5632def"
. Значит, это уже не один ключ, и ваша StringLiteral
утилита неправильно его классифицирует. Вы понимаете?
• Поскольку я поместил проверки в функции-члены, чтобы убедиться, что записи содержат ключ, я решил изучить, можно ли использовать систему типов, чтобы избежать необходимости в проверках. • Спасибо за дополнительную точность, теперь я понимаю, что вы имеете в виду, и определенно узнал здесь что-то новое о Typescript. StringLiteral<K>
не будет ограничивать K
одним строковым литералом, как вы изначально сказали, это была напрасная трата усилий. Более простой MyClass<K extends string>
дал бы аналогичный результат.
Я думаю, что вам потребуются проверки во время выполнения, несмотря ни на что, но я не вижу этих проверок в вашем коде, поэтому я не уверен, как вы вообще определяете, какой ключ вы ищете (здесь код просто имеет ключ как тип , поэтому среда выполнения не знает об этом.) Вы можете попытаться ограничиться одним строковым литералом, но это сложно и, вероятно, хрупко. ... Так что же вы хотите здесь увидеть? Ответ «не пытайтесь это сделать»? Ответ, показывающий, насколько мы можем приблизиться к обеспечению одного строкового литерала?
Подробно о сценарии использования: MyClass
передает запись внешней подсистеме, которая выполняет индексацию; Сначала я написал методы MyClass
с проверками, такими как Object.keys(arg).includes(this.primaryKey);
, прежде чем использовать вместо них типы. Мне интересно понять, как работает ваша игровая площадка, с оговоркой, что может быть полезно только убедиться в наличии указанного ключа.
Посетите stackoverflow.com/questions/60872063/…
Боюсь, я до сих пор не вижу варианта использования, который требует, чтобы тип был одной строкой литеральным типом, но в дальнейшем я посмотрю, насколько близко мы можем подойти.
Во-первых, мы можем написать служебный тип 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, это было очень поучительно!
Каков основной вариант использования? Что плохого произойдет, если у
arg
более одного ключа? Потому что что бы ты ни делал в ТС, такое всё равно может случиться . Попытка заставить TS не допускать дополнительные свойства — это, по большому счету, напрасная трата усилий. Пожалуйста, отредактируйте, чтобы уточнить вариант использования; вполне возможно, что есть подход, который лучше подходит для этого.