Я пытаюсь написать функцию, которая будет принимать строковый литерал и возвращать объект с одним полем, имя которого является этим строковым литералом. Я могу написать функцию, которая делает то, что я хочу, но я не знаю, как выразить ограничение, что тип ее аргумента должен быть строковым литералом.
Самое близкое, что у меня есть, это использование универсального типа, который расширяет string
. Это разрешает типы строковых литералов, а также объединения типов строковых литералов и типа string
, которые я не хочу передавать в свою функцию.
Это компилируется и делает то, что я хочу, при условии, что K
является строковым литералом. Обратите внимание, что в typescript 3.4 утверждение типа не требовалось, но требуется в 3.5.
function makeObject<K extends string>(key: K): { [P in K]: string } {
return { [key]: "Hello, World!" } as { [P in K]: string };
}
Если K
не является строковым литералом, возвращаемый тип этой функции не будет таким же, как тип возвращаемого значения.
Два пути, которые я могу представить для выполнения этой работы:
Может ли система типов машинописного текста выражать любой из них?
Если я удаляю утверждение типа в typescript 3.5, я получаю сообщение об ошибке:
a.ts:2:3 - error TS2322: Type '{ [x: string]: string; }' is not assignable to type '{ [P in K]: string; }'.
2 return { [key]: "Hello, World!" };
В идеале ваш пример приведет к ошибке типа, потому что аргумент представляет собой объединение строковых литералов, а не строковый литерал.
Нет никаких ограничений на то, чтобы что-то было однострочным литералом. Если вы укажете extends string
, компилятор выведет типы строковых литералов для K
, но он также по определению разрешит объединения типов строковых литералов (ведь набор объединения типов строковых литералов включен в набор всех строк)
Мы можем создать пользовательскую ошибку, которая заставляет as call находиться в состоянии ошибки, если обнаруживает объединение типов строковых литералов. Такую проверку можно сделать с помощью условных типов, убедившись, что K
совпадает с UnionToIntersection<K>
. Если это так, то K
не является союзом, так как 'a' extends 'a'
но 'a' | 'b'
не распространяется 'a' & 'b'
type UnionToIntersection<U> =
(U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never
type CheckForUnion<T, TErr, TOk> = [T] extends [UnionToIntersection<T>] ? TOk : TErr
function makeObject<K extends string>(key: K & CheckForUnion<K, never, {}>): { [P in K]: string } {
return { [key]: "Hello, World!" } as { [P in K]: string };
}
makeObject("a")
makeObject("a" as "a" | "b") // Argument of type '"a" | "b"' is not assignable to parameter of type 'never'
Какова цель написания типов кортежей [T] extends [UnionToIntersection<T>]
? Это просто для предотвращения распространения условия по объединению в случае, когда T
является типом объединения?
Кроме того, для объяснения того, почему UnionToIntersection
работает, см. stackoverflow.com/questions/50374908/….
Обновление для TypeScript 4.2
Следующие работы:
type StringLiteral<T> = T extends string ? string extends T ? never : T : never;
(Больше не работает): трюк с буквальным типом шаблона TypeScript 4.1
Обновлено: приведенное ниже фактически сломалось в 4.2. Обсуждение здесь
type StringLiteral<T> = T extends `${string & T}` ? T : never;
В TS 4.1 представлен литеральные типы шаблонов, который позволяет преобразовывать строковые литералы в другие строковые литералы. Вы можете преобразовать строковый литерал в себя. Поскольку шаблонами могут быть только литералы, а не общие строки, вы просто проверяете условно, что строковый литерал наследуется от самого себя.
Полный пример:
type StringLiteral<T> = T extends `${string & T}` ? T : never;
type CheckLiteral = StringLiteral<'foo'>; // type is 'foo'
type CheckString = StringLiteral<string>; // type is never
function makeObject<K>(key: StringLiteral<K>) {
return { [key]: 'Hello, World!' } as { [P in StringLiteral<K>]: string };
}
const resultWithLiteral = makeObject('hello'); // type is {hello: string;}
let someString = 'prop';
const resultWithString = makeObject(someString); // compiler error.
Я не думаю, что объединение для K
больше не является проблемой, потому что нет необходимости сужать тип для ключа свойства в подписи makeObject
. Во всяком случае, это становится более гибким.
Можно ли превратить универсальный строковый литерал в фактическое строковое значение?
Хотя не уверен, каким должен быть результат. что должно
makeObject("a" as "a" | "b");
вернуть?{ a?: string; b?: string; }
может? мы можем получить триггер ошибки, еслиK
не является однострочным литералом (т.е. не объединением), это то, что вы хотите?