#typescript #generics #generic-constraints #keyof
#typescript #общие #generic-ограничения #keyof
Вопрос:
В TypeScript это не компиляция:
export interface Generic<T extends string> {
}
export interface Class<T extends string[]> {
readonly prop: { [P in keyof T]: Generic<T[P]> }
}
В частности, Generic<T[P]>
сбой с Type 'T[P]' does not satisfy the constraint 'string'.
помощью . Однако, поскольку T extends string[]
, можно быть уверенным, что T[P] extends string
для любого P in keyof T
.
Что я здесь делаю не так?
Я знаю, что могу решить проблему с помощью условного типа:
export interface Class<T extends string[]> {
readonly prop: { [P in keyof T]: T[P] extends string ? Generic<T[P]> : never }
}
Но я не понимаю, зачем это нужно.
Комментарии:
1. Не
P
может бытьstring
,number
илиSymbol
?2. @Anatoly да, это возможно. Однако я не понимаю вашей точки зрения.
Ответ №1:
Если вы посмотрите на полную ошибку, третья строка содержит большую подсказку:
Type 'T[P]' does not satisfy the constraint 'string'.
Type 'T[keyof T]' is not assignable to type 'string'.
Type 'T[string] | T[number] | T[symbol]' is not assignable to type 'string'.
Type 'T[string]' is not assignable to type 'string'.(2344)
Проблема в том, что keyof
любой тип массива (или тип кортежа) будет больше похож string | number | symbol
. И массив также имеет больше, чем тип элемента для этих ключей. Например:
// (...items: string[]) => number
type PushFunction = string[]['push']
Смотрите этот фрагмент. В ключах массива гораздо больше, чем просто числа:
// number | "0" | "1" | "2" | "length" | "toString"
// | "toLocaleString" | "pop" | "push" | "concat"
// | "join" | "reverse" | "shift" | "slice" | "sort"
// | "splice" | "unshift" | "indexOf"
// | ... 15 more ... | "includes"
type ArrayKeys = keyof [1,2,3]
И Generic<T>
должен T
быть строкой, но, как показано, не все значения всех ключей массива являются строками.
Вы можете очень просто исправить отображаемый тип, пересекая ключи массива с number
, сообщая typescript, что вас интересуют только цифровые ключи (которые являются индексами массива):
export interface Generic<T extends string> {
}
export interface Class<T extends string[]> {
readonly prop: { [P in keyof T amp; number]: Generic<T[P]> }
}
Ответ №2:
Это известная ошибка в TypeScript, из-за которой поддержка (добавленная в TS3.1) для отображаемых типов поверх кортежей и массивов не существует в реализации таких отображаемых типов; см. microsoft / TypeScript #27995 . Похоже, что, по словам ведущего архитектора TypeScript:
Проблема здесь в том, что мы сопоставляем типы кортежей и массивов только при создании экземпляра общего гомоморфного отображаемого типа для кортежа или массива (см. #26063).
Таким образом, извне, когда вы используете сопоставленный тип для массива или кортежа, сопоставление сохраняет массив или последовательность входных данных и отображает только числовые свойства:
declare const foo: Class<["a", "b", "c"]>;
// (property) prop: [Generic<"a">, Generic<"b">, Generic<"c">]
const zero = foo.prop[0]; // Generic<"a">;
const one = foo.prop[1]; // Generic<"b">;
но внутри компилятор по-прежнему видит P in keyof T
итерацию по каждому ключу T
, включая любые, возможно, нечисловые.
export interface Class<T extends string[]> {
readonly prop: { [P in keyof T]: Generic<T[P]> } // error!
}
Как вы заметили, для этого существуют обходные пути, и эти обходные пути упоминаются в microsoft / TypeScript # 27995 . Я думаю, что лучший из них по сути такой же, как ваш условный тип:
export interface Class<T extends string[]> {
readonly prop: { [P in keyof T]: Generic<Extract<T[P], string>> }
}
Другие из них либо не работают для общих типов, таких как T
, либо создают сопоставленные типы, которые больше не являются настоящими массивами или кортежами (например, {0: Generic<"a">, 1: Generic<"b">, 2: Generic<"c">}
вместо [Generic<"a">, Generic<"b">, Generic<"c">]
… поэтому я оставлю их вне этого ответа.
Ответ №3:
Обратите внимание, что for T extends string[]
, keyof T
включает в себя не только цифровые клавиши, но и все методы, образующие прототип массива. Доказательство? Вот оно:
type StringArrayKeys = keyof string[];
// Produces:
// type StringArrayKeys = number | "length" | "toString" | "toLocaleString" | "pop" |
// "push" | "concat" | "join" | "reverse" | "shift" | "slice" | "sort" | "splice" |
// "unshift" | "indexOf" | "lastIndexOf" | ... 16 more
Итак, самым простым решением для вашего примера было бы заменить P in keyof T
на P in number
:
export interface Generic<T extends string> {};
export interface Class<T extends string[]> {
readonly prop: { [P in number]: Generic<T[P]> }
}