Index Signatures

До Object у JavaScript (і, отже, TypeScript) можна отримати доступ за допомогою string який містить посилання на будь-який інший JavaScript object.

Ось короткий приклад:

let foo: any = {};
foo['Hello'] = 'World';
console.log(foo['Hello']); // World

Ми зберігаємо рядок "World" під ключем "Hello". Пам’ятайте, ми сказали, що він може зберігати будь-який object JavaScript, тому давайте збережемо екземпляр класу, щоб показати концепцію:

class Foo {
  constructor(public message: string){};
  log(){
    console.log(this.message)
  }
}

let foo: any = {};
foo['Hello'] = new Foo('World');
foo['Hello'].log(); // World

акож пам’ятайте, що ми сказали, що до нього можна отримати доступ за допомогою string. Якщо ви передаєте будь-який інший об’єкт до сигнатури індексу, середовище виконання JavaScript фактично викликає для нього .toString перед отриманням результату. Це показано нижче:

let obj = {
  toString(){
    console.log('toString called')
    return 'Hello'
  }
}

let foo: any = {};
foo[obj] = 'World'; // toString called
console.log(foo[obj]); // toString called, World
console.log(foo['Hello']); // World

Зауважте, що toString буде викликатися кожного разу, коли obj використовується в позиції індексу.

Масиви трохи відрізняються. Віртуальні машини JavaScript намагатимуться оптимізувати індексацію за числом (залежно від того, чи це насправді масив, чи збігаються структури збережених елементів тощо). Тому number слід розглядати як дійсний засіб доступу до об’єкта сам по собі (на відміну від string). Ось простий приклад масиву:

let foo = ['World'];
console.log(foo[0]); // World

Отже, це JavaScript. Тепер давайте подивимося, як TypeScript витончено використовує цю концепцію.

TypeScript Index Signature

По-перше, оскільки JavaScript implicitly викликає toString для будь-якого підпису індексу об’єкта, TypeScript видасть вам помилку, щоб запобігти початківцям стріляти собі в ногу (я бачу, як користувачі стріляють собі в ногу, коли весь час використовують JavaScript на stackoverflow ):

let obj = {
  toString(){
    return 'Hello'
  }
}

let foo: any = {};

// ERROR: the index signature must be string, number ...
foo[obj] = 'World';

// FIX: TypeScript forces you to be explicit
foo[obj.toString()] = 'World';

Причина примушування користувача бути явним полягає в тому, що реалізація toString за замовчуванням для об’єкта є досить жахливою, напр. на v8 завжди повертає [object Object]:

let obj = {message:'Hello'}
let foo: any = {};

// ERROR: the index signature must be string, number ...
foo[obj] = 'World';

// Here is where you actually stored it!
console.log(foo["[object Object]"]); // World

Звичайно, number підтримується, оскільки

  1. це необхідно для відмінної підтримки масиву / кортежу.

  2. навіть якщо ви використовуєте його для obj, його типова реалізація toString є гарною (а не [object Object]).

Пункт 2 показано нижче:

console.log((1).toString()); // 1
console.log((2).toString()); // 2

Отже, урок 1:

Сигнатури індексу TypeScript мають бути або string або number

Коротка примітка: symbols також дійсні та підтримуються TypeScript. Але не будемо поки що туди. Маленькі кроки.

Declaring an index signature

Тож ми використовували any, щоб сказати TypeScript дозволяти нам робити все, що ми хочемо. Насправді ми можемо вказати підпис index явно. наприклад скажімо, ви хочете переконатися, що все, що зберігається в об’єкті за допомогою рядка, відповідає структурі {message: string}. Це можна зробити за допомогою оголошення { [index:string] : {message: string} }. Це показано нижче:

let foo:{ [index:string] : {message: string} } = {};

/**
 * Must store stuff that conforms to the structure
 */
/** Ok */
foo['a'] = { message: 'some message' };
/** Error: must contain a `message` of type string. You have a typo in `message` */
foo['a'] = { messages: 'some message' };

/**
 * Stuff that is read is also type checked
 */
/** Ok */
foo['a'].message;
/** Error: messages does not exist. You have a typo in `message` */
foo['a'].messages;

ПОРАДА: назва підпису індексу, напр. index в { [index:string] : {message: string} } не має значення для TypeScript і призначений лише для читабельності. напр. якщо це імена користувачів, ви можете зробити { [username:string] : {message: string} }, щоб допомогти наступному розробнику, який перегляне код (яким випадково можете бути ви).

Звичайно, також підтримуються «числові» індекси, напр. { [count: number] : SomeOtherTypeYouWantToStoreEgRebate }

All members must conform to the string index signature

Як тільки у вас є підпис індексу string усі явні члени також повинні відповідати цьому підпису індексу. Це показано нижче:

/** Okay */
interface Foo {
  [key:string]: number;
  x: number;
  y: number;
}
/** Error */
interface Bar {
  [key:string]: number;
  x: number;
  y: string; // ERROR: Property `y` must be of type number
}

Це робиться для забезпечення безпеки, щоб будь-який доступ до рядка давав однаковий результат:

interface Foo {
  [key:string]: number;
  x: number;
}
let foo: Foo = {x:1,y:2};

// Directly
foo['x']; // number

// Indirectly
let x = 'x'
foo[x]; // number

Using a limited set of string literals

Сигнатура індексу може вимагати, щоб рядки індексу були членами об’єднання літеральних рядків за допомогою Mapped Types наприклад:

type Index = 'a' | 'b' | 'c'
type FromIndex = { [k in Index]?: number }

const good: FromIndex = {b:1, c:2}

// Помилка:
// Типу '{ b: число; в: число; d: число; }" не можна призначити типу "FromIndex".
// Літерал об’єкта може вказувати лише відомі властивості, а «d» не існує в типі «FromIndex».
const bad: FromIndex = {b:1, c:2, d:3};

Це часто використовується разом із keyof typeof для захоплення типів словника, описаного на наступній сторінці.

Специфікація словника може бути відкладена узагальнено:

type FromSomeIndex<K extends string> = { [key in K]: number }

Having both string and number indexers

Це не поширений випадок використання, але компілятор TypeScript все одно підтримує його.

Однак він має обмеження, що індексатор string є більш суворим, ніж індексатор number. Це навмисно, напр. щоб дозволити вводити такі речі, як:

interface ArrStr {
  [key: string]: string | number; // Must accommodate all members

  [index: number]: string; // Can be a subset of string indexer

  // Just an example member
  length: number;
}

Design Pattern: Nested index signature

Розгляд API під час додавання підписів індексу

Досить часто в спільноті JS ви бачите API, які зловживають індексаторами рядків. напр. загальний шаблон серед CSS у бібліотеках JS:

interface NestedCSS {
  color?: string;
  [selector: string]: string | NestedCSS | undefined;
}

const example: NestedCSS = {
  color: 'red',
  '.subclass': {
    color: 'blue'
  }
}

Намагайтеся не змішувати рядкові індексатори з valid значеннями таким чином. наприклад помилка в заповненні залишиться невиявленою:

const failsSilently: NestedCSS = {
  colour: 'red', // No error as `colour` is a valid string selector
}

Натомість відокремте вкладення у власну властивість, наприклад. в назві на зразок nest (або children або subnodes тощо):

interface NestedCSS {
  color?: string;
  nest?: {
    [selector: string]: NestedCSS;
  }
}

const example: NestedCSS = {
  color: 'red',
  nest: {
    '.subclass': {
      color: 'blue'
    }
  }
}

const failsSilently: NestedCSS = {
  colour: 'red', // TS Error: unknown property `colour`
}

Excluding certain properties from the index signature

Іноді потрібно об’єднати властивості в підпис індексу. Це не рекомендовано, і ви повинні uвикористовувати шаблон підпису вкладеного індексу, згаданий вище.

Однак, якщо ви моделюєте existing JavaScript ви можете обійти його за допомогою типу перехрестя. Нижче показано приклад помилки, з якою ви зіткнетеся без використання перехрестя:

type FieldState = {
  value: string
}

type FormState = {
  isValid: boolean  // Error: Does not conform to the index signature
  [fieldName: string]: FieldState
}

Ось обхідний шлях за допомогою типу перетину:

type FieldState = {
  value: string
}

type FormState =
  { isValid: boolean }
  & { [fieldName: string]: FieldState }

Зауважте, що навіть якщо ви можете оголосити його для моделювання існуючого JavaScript, ви не можете створити такий об’єкт за допомогою TypeScript:

type FieldState = {
  value: string
}

type FormState =
  { isValid: boolean }
  & { [fieldName: string]: FieldState }


// Use it for some JavaScript object you are getting from somewhere 
declare const foo:FormState; 

const isValidBool = foo.isValid;
const somethingFieldState = foo['something'];

// Using it to create a TypeScript object will not work
const bar: FormState = { // Error `isValid` not assignable to `FieldState
  isValid: false
}

Last updated