TypeScript — сохранение типов отображения ограниченного общего кортежа

#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]> }
}