Отримати ключі інтерфейсу Typescript як масив рядків


112

У мене багато таблиць у Lovefield та їх відповідних інтерфейсах для того, які стовпці вони мають.
Приклад:

export interface IMyTable {
  id: number;
  title: string;
  createdAt: Date;
  isDeleted: boolean;
}

Я хотів би мати імена властивостей цього інтерфейсу в такому масиві:

const IMyTable = ["id", "title", "createdAt", "isDeleted"];

я не можу зробити об’єкт / масив безпосередньо на основі інтерфейсу, IMyTableякий повинен зробити трюк, оскільки я б отримував імена інтерфейсів таблиць динамічно. Отже, мені потрібно переглядати ці властивості в інтерфейсі та отримувати з нього масив.

Як мені досягти цього результату?

Відповіді:


55

Починаючи з TypeScript 2.3 (або я повинен сказати 2.4 , оскільки в 2.3 ця функція містить помилку, яку було виправлено в typecript@2.4-dev ), ви можете створити власний трансформатор, щоб досягти того, що ви хочете зробити.

Власне, я вже створив такий власний трансформатор, який дозволяє наступне.

https://github.com/kimamula/ts-transformer-keys

import { keys } from 'ts-transformer-keys';

interface Props {
  id: string;
  name: string;
  age: number;
}
const keysOfProps = keys<Props>();

console.log(keysOfProps); // ['id', 'name', 'age']

На жаль, користувальницькі трансформатори в даний час не так прості у використанні. Вам доведеться використовувати їх із API перетворення TypeScript замість виконання команди tsc. Виникла проблема із запитом на підтримку плагіна для власних трансформаторів.


Дякую за вашу відповідь, я вже вчора бачив і встановлював цей користувальницький трансформатор, але оскільки тут використовується машинопис 2.4, то від цього мені це зараз ніякої користі.
— Тушар Шукла

17
Привіт, ця бібліотека також відповідає точно моїм вимогам, однак, я отримую, ts_transformer_keys_1.keys is not a functionколи виконую точні дії в документації. чи є вирішення цього?
— Hasitha Shan

Охайно! Як ви думаєте, чи можна його розширити, щоб прийняти параметр динамічного типу (примітка 2 у readme)?
— kenshin

@HasithaShan уважно роздивіться документи - для того, щоб пакет працював, вам потрібно використовувати API компілятора TypeScript
— Ярослав Бай,

3
На жаль, пакет зламаний, що б я не робив, я завжди отримуюts_transformer_keys_1.keys is not a function
— fr1sk

20

Наведене нижче вимагає, щоб ви самостійно перерахували ключі, але принаймні TypeScript буде застосовувати IUserProfileі IUserProfileKeysмати точно такі самі ключі ( Required<T>було додано в TypeScript 2.8 ):

export interface IUserProfile  {
  id: string;
  name: string;
};
type KeysEnum<T> = { [P in keyof Required<T>]: true };
const IUserProfileKeys: KeysEnum<IUserProfile> = {
  id: true,
  name: true,
};

Досить крутий фокус. Тепер легко застосувати реалізацію всіх ключів IUserProfileта легко витягти їх із const IUserProfileKeys. Це саме те, що я шукав. Не потрібно зараз перетворювати всі мої інтерфейси на класи.
— Анддо

17

У мене була подібна проблема, що у мене був гігантський перелік властивостей, для яких я хотів би мати як інтерфейс, так і об’єкт поза ним.

ПРИМІТКА: Я не хотів писати (друкувати за допомогою клавіатури) властивості двічі! Просто СУХИЙ.


Тут слід зауважити одне: інтерфейси є примусовими типами під час компіляції, тоді як об’єкти в основному виконуються. ( Джерело )

Як @derek згадано в іншій відповіді , загальним знаменником інтерфейсу та об'єкта може бути клас, який обслуговує як тип, так і значення .

Отже, TL; DR, такий фрагмент коду повинен задовольняти потреби:

class MyTableClass {
    // list the propeties here, ONLY WRITTEN ONCE
    id = "";
    title = "";
    isDeleted = false;
}

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

// This is the pure interface version, to be used/exported
interface IMyTable extends MyTableClass { };

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

// Props type as an array, to be exported
type MyTablePropsArray = Array<keyof IMyTable>;

// Props array itself!
const propsArray: MyTablePropsArray =
    Object.keys(new MyTableClass()) as MyTablePropsArray;

console.log(propsArray); // prints out  ["id", "title", "isDeleted"]


// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

// Example of creating a pure instance as an object
const tableInstance: MyTableClass = { // works properly!
    id: "3",
    title: "hi",
    isDeleted: false,
};

( Ось наведений вище код у Typescript Playground, щоб грати більше)

PS. Якщо ви не хочете призначати початкові значення властивостям у класі і залишатися з типом, ви можете виконати фокус конструктора:

class MyTableClass {
    // list the propeties here, ONLY WRITTEN ONCE
    constructor(
        readonly id?: string,
        readonly title?: string,
        readonly isDeleted?: boolean,
    ) {}
}

console.log(Object.keys(new MyTableClass()));  // prints out  ["id", "title", "isDeleted"] 

Трюк конструктора на майданчику TypeScript .


Доступний propsArrayлише тоді, коли ви ініціалізували ключі.
— denkquer

Я не розумію, що ви маєте на увазі під "ініціалізованим" @denkquer. У першому прикладі propsArrayдоступний перед, tableInstanceякщо це те, що ви маєте на увазі, так чітко перед ініціалізацією екземпляра. Однак, якщо ви маєте на увазі підроблені значення, присвоєні в MyTableClass, вони просто знаходяться там, щоб незабаром зрозуміти "тип" властивостей. Якщо ви їх не хочете, можете скористатися трюком конструктора в прикладі PS.
— Айдін

1
На моє розуміння, значення ініціалізується, коли воно має будь-яке значення. Ваш "трюк конструктора" вводить в оману, тому що ви не можете просто замінити його MyTableClassостаннім і очікувати отримання ключів у тому випадку, propsArrayколи неініціалізовані vars і типи позбавляються під час виконання. Ви завжди повинні надавати їм якесь значення за замовчуванням. Я виявив, що їх ініціалізація undefined- найкращий підхід.
— denkquer

1
@Aidin дякую за ваше рішення. Мені також цікаво, чи можу я уникнути ініціалізації параметрів. Якщо я використовую трюк конструктора, я більше не можу створити інтерфейс, який розширює MyTableClass .. твій трюк конструктора в посиланні на ігровий майданчик машинопису порожній
— Flion,

1
@Flion, дякую, що це помітили. Я щойно оновив посилання на ігровий майданчик для трюку конструктора. Подивіться, чи працює це зараз.
— Айдін

10

Це має спрацювати

var IMyTable: Array<keyof IMyTable> = ["id", "title", "createdAt", "isDeleted"];

або

var IMyTable: (keyof IMyTable)[] = ["id", "title", "createdAt", "isDeleted"];

15
Не те, що це неправильно, але, щоб бути зрозумілим тут, ви просто "примушуєте значення масиву" бути правильними. Розробнику все одно потрібно записати їх двічі, вручну.
— Айдін

Хоча те, що сказав Ейдін, може бути правдою, для деяких випадків це саме те, що я шукав, для мого випадку. Дякую.
— Деніел

4
Це не запобіжить дублікатам ключів або відсутнім ключам. Подобаєтьсяvar IMyTable: Array<keyof IMyTable> = ["id", "createdAt", "id"];
— ford04

Для мене це також було те, що я шукав, тому що я хочу додатково прийняти клавіші, але не більше, ніж клавіші, визначені в інтерфейсі. Не очікував, що це буде за замовчуванням із наведеним вище кодом. Думаю, нам все одно знадобиться загальний спосіб TS для цього. У будь-якому випадку дякую за наведений вище код!
— nicoes

10

Можливо, це вже пізно, але у версії 2.1 TypeScript ви можете використовувати key ofтак:

interface Person {
    name: string;
    age: number;
    location: string;
}

type K1 = keyof Person; // "name" | "age" | "location"
type K2 = keyof Person[];  // "length" | "push" | "pop" | "concat" | ...
type K3 = keyof { [x: string]: Person };  // string

Документ: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html#keyof-and-lookup-types


Дякую за відповідь, але не впевнений, чи допомагає комусь використовувати статично створені типи з інтерфейсу. IMHO, ми можемо використовувати інтерфейси / типи взаємозамінними у більшості випадків. До того ж для цього знадобиться ручне створення типів для декількох інтерфейсів. Однак рішення виглядає непогано, якщо комусь просто потрібно вивести типи з інтерфейсу.
— Тушар Шукла

8

Безпечні варіанти

Створення масиву або кортежу ключів від інтерфейсу з перевіркою безпеки під час компіляції вимагає трохи творчості. Типи стираються під час виконання, і типи об’єктів (невпорядковані, іменовані) не можуть бути перетворені в типи кортежів (впорядковані, неіменовані), не вдаючись до непідтримуваних технік .

Порівняння з іншими відповідями

Всі запропоновані тут варіанти розглядають / запускають помилку компіляції у випадку повторення або відсутності елементів кортежу з заданим типом об’єкта посилання IMyTable. Наприклад, оголошення типу масиву (keyof IMyTable)[]не може виявити ці помилки.

Крім того, вони не потребують певної бібліотеки (останній варіант використовує ts-morph, який я вважав би загальним обгорткою компілятора), видають тип кортежу на відміну від об’єкта (лише перше рішення створює масив) або широкого типу масиву (порівняйте з ці відповіді ) і, нарешті , не потребують занять .

Варіант 1: Простий набраний масив

// Record type ensures, we have no double or missing keys, values can be neglected
function createKeys(keyRecord: Record<keyof IMyTable, any>): (keyof IMyTable)[] {
  return Object.keys(keyRecord) as any
}

const keys = createKeys({ isDeleted: 1, createdAt: 1, title: 1, id: 1 })
// const keys: ("id" | "title" | "createdAt" | "isDeleted")[]

+найпростіший +-посібник із -масивом автозаповнення , без кортежу

Дитячий майданчик

Якщо вам не подобається створювати запис, подивіться на цю альтернативу з Setтипами тверджень та твердженнями .


Варіант 2: Кортеж з допоміжною функцією

function createKeys<T extends readonly (keyof IMyTable)[] | [keyof IMyTable]>(
    t: T & CheckMissing<T, IMyTable> & CheckDuplicate<T>): T {
    return t
}

++-посібник з кортежами з автоматичним заповненням +-більш досконалих, складних типів

Дитячий майданчик

Пояснення

createKeysробить перевірки під час компіляції , об'єднуючи тип параметра функції з додатковими типами тверджень, які видають помилку для непридатного введення. (keyof IMyTable)[] | [keyof IMyTable]є "чорною магією" способом примусити зробити висновок кортежу замість масиву з боку викликаного. Крім того, ви можете використовувати твердження const /as const з боку абонента.

CheckMissingперевіряє, якщо Tпропустив ключі від U:

type CheckMissing<T extends readonly any[], U extends Record<string, any>> = {
    [K in keyof U]: K extends T[number] ? never : K
}[keyof U] extends never ? T : T & "Error: missing keys"

type T1 = CheckMissing<["p1"], {p1:any, p2:any}> //["p1"] & "Error: missing keys"
type T2 = CheckMissing<["p1", "p2"], { p1: any, p2: any }> // ["p1", "p2"]

Примітка: T & "Error: missing keys"саме для приємних помилок IDE. Ви також могли писати never. CheckDuplicatesперевіряє подвійні елементи кортежу:

type CheckDuplicate<T extends readonly any[]> = {
    [P1 in keyof T]: "_flag_" extends
    { [P2 in keyof T]: P2 extends P1 ? never :
        T[P2] extends T[P1] ? "_flag_" : never }[keyof T] ?
    [T[P1], "Error: duplicate"] : T[P1]
}

type T3 = CheckDuplicate<[1, 2, 3]> // [1, 2, 3]
type T4 = CheckDuplicate<[1, 2, 1]> 
// [[1, "Error: duplicate"], 2, [1, "Error: duplicate"]]

Примітка: Більше інформації про перевірку унікальних предметів у кортежах міститься в цій публікації . За допомогою TS 4.1 ми також можемо називати відсутні ключі в рядку помилок - погляньте на цей майданчик .


Варіант 3: рекурсивний тип

З версією 4.1 TypeScript офіційно підтримує умовні рекурсивні типи , які також можуть бути використані тут. Хоча обчислення типів є дорогими через комбінаційну складність - продуктивність значно погіршується для більш ніж 5-6 елементів. Я перелічую цю альтернативу для повноти ( дитячий майданчик ):

type Prepend<T, U extends any[]> = [T, ...U] // TS 4.0 variadic tuples

type Keys<T extends Record<string, any>> = Keys_<T, []>
type Keys_<T extends Record<string, any>, U extends PropertyKey[]> =
  {
    [P in keyof T]: {} extends Omit<T, P> ? [P] : Prepend<P, Keys_<Omit<T, P>, U>>
  }[keyof T]

const t1: Keys<IMyTable> = ["createdAt", "isDeleted", "id", "title"] // ✔

+Кортеж +-керівництво з автозаповненням +НЕ допоміжна функція --продуктивності


Варіант 4: API генератора коду / компілятора TS

Тут вибрано ts-morph , оскільки це трохи простіша обгортка, альтернатива оригінальному API компілятора TS . Звичайно, ви також можете використовувати API компілятора безпосередньо. Давайте розглянемо код генератора:

// ./src/mybuildstep.ts
import {Project, VariableDeclarationKind, InterfaceDeclaration } from "ts-morph";

const project = new Project();
// source file with IMyTable interface
const sourceFile = project.addSourceFileAtPath("./src/IMyTable.ts"); 
// target file to write the keys string array to
const destFile = project.createSourceFile("./src/generated/IMyTable-keys.ts", "", {
  overwrite: true // overwrite if exists
}); 

function createKeys(node: InterfaceDeclaration) {
  const allKeys = node.getProperties().map(p => p.getName());
  destFile.addVariableStatement({
    declarationKind: VariableDeclarationKind.Const,
    declarations: [{
        name: "keys",
        initializer: writer =>
          writer.write(`${JSON.stringify(allKeys)} as const`)
    }]
  });
}

createKeys(sourceFile.getInterface("IMyTable")!);
destFile.saveSync(); // flush all changes and write to disk

Після компіляції та запуску цього файлу генерується tsc && node dist/mybuildstep.jsфайл ./src/generated/IMyTable-keys.tsіз таким вмістом:

// ./src/generated/IMyTable-keys.ts
const keys = ["id","title","createdAt","isDeleted"] as const;

+автоматичні породжує рішення +масштабується для декількох властивостей +НЕ допоміжної функції +кортежу -додаткової збірки кроку -не потрібно знайомство з компілятором API


6

Замість того, щоб визначати IMyTableяк в інтерфейсі, спробуйте визначити його як клас. У машинописі ви можете використовувати клас, такий як інтерфейс.

Отже, для вашого прикладу визначте / сформуйте свій клас так:

export class IMyTable {
    constructor(
        public id = '',
        public title = '',
        public createdAt: Date = null,
        public isDeleted = false
    )
}

Використовуйте його як інтерфейс:

export class SomeTable implements IMyTable {
    ...
}

Отримати ключі:

const keys = Object.keys(new IMyTable());

5

Вам потрібно буде створити клас, який реалізує ваш інтерфейс, створити екземпляр його, а потім використовувати Object.keys(yourObject)для отримання властивостей.

export class YourClass implements IMyTable {
    ...
}

тоді

let yourObject:YourClass = new YourClass();
Object.keys(yourObject).forEach((...) => { ... });

У моєму випадку це не працює, мені довелося б перерахувати ці властивості інтерфейсу, але це не те, що я хочу? Назва інтерфейсу приходить динамічно, і тоді я повинен визначити його властивості
— Тушар Шукла,

Це призводить до помилки (v2.8.3): Cannot extend an interface […]. Did you mean 'implements'?Однак використання implementsзамість цього вимагає копіювання інтерфейсу вручну, а саме цього я не хочу.
— Яків,

@jacob вибачте, це мало бути, implementsі я оновив свою відповідь. Як зазначив @basarat, інтерфейси не існують під час виконання, тому єдиний спосіб - це реалізувати його як клас.
— Ден Деф

Ви маєте на увазі замість інтерфейсу використовувати клас? На жаль, я не можу, оскільки інтерфейс походить від сторонніх розробників ( @types/react). Я скопіював їх вручну, але це навряд чи забезпечить майбутнє 😪 Я намагаюся динамічно прив’язати нежиттєві методи (які вже пов’язані), але вони не оголошені в React.Component (клас).
— Яків,

Ні, я маю на увазі створити клас, який реалізує ваш сторонній інтерфейс, і отримати властивості цього класу під час виконання.
— Ден Деф

3

Не може. Інтерфейси не існують під час виконання.

обхідний шлях

Створіть змінну типу та використовуйте Object.keysна ній 🌹


1
Ви маєте на увазі var abc: IMyTable = {}; Object.keys(abc).forEach((key) => {console.log(key)});
— наступне

4
Ні, бо на цьому об’єкті немає ключів. Інтерфейс - це те, що TypeScript використовує, але випаровується в JavaScript, тому не залишається жодної інформації, яка б повідомляла про будь-яке "відображення" або "взаємоперегляд". Все, що відомо JavaScript, - це порожній літерал об’єкта. Ваша єдина надія - дочекатися (або запитати ), що TypeScript включає спосіб генерування масиву або об’єкта з усіма ключами в інтерфейсі у вихідний код. Або, як каже Ден Деф, якщо ви зможете використовувати клас, у вас у кожному випадку будуть визначені ключі у вигляді властивостей ..
— Jesper

17
Якщо це не спрацьовує, чому голоси за цю відповідь?
— dawez

2
Причина проти: не згадується, що це не працює для значень, що
— допускають обнулення

2
Врешті-решт це не чудове рішення, тому що ви повинні надати значення. Можливо, краще просто вести список ключів.
— Деніел Томпсон,

0

Це було важко! Дякую всім за допомогу.

Мені потрібно було отримати ключі інтерфейсу як масив рядків для спрощення сценаріїв mocha / chai. Ви ще не стурбовані використанням програми (поки що), тому не потрібно було створювати файли ts. Дякуємо ford04 за допомогу, його рішення вище дуже допомогло, і воно працює чудово, НЕ злом компілятора. Ось модифікований код:

Варіант 2: Генератор коду на основі API компілятора TS (ts-morph)

Вузольний модуль

npm install --save-dev ts-morph

keys.ts

ПРИМІТКА : це передбачає, що всі файли ts знаходяться в кореневій папці ./src і відсутні вкладені папки, налаштуйте відповідно

import {
  Project,
  VariableDeclarationKind,
  InterfaceDeclaration,
} from "ts-morph";

// initName is name of the interface file below the root, ./src is considered the root
const Keys = (intName: string): string[] => {
  const project = new Project();
  const sourceFile = project.addSourceFileAtPath(`./src/${intName}.ts`);
  const node = sourceFile.getInterface(intName)!;
  const allKeys = node.getProperties().map((p) => p.getName());

  return allKeys;
};

export default Keys;

використання

import keys from "./keys";

const myKeys = keys("MyInterface") //ts file name without extension

console.log(myKeys)

-1

Ви не можете цього зробити. Інтерфейси не існують під час виконання (як @basarat сказав).

Зараз я працюю з наступними:

const IMyTable_id = 'id';
const IMyTable_title = 'title';
const IMyTable_createdAt = 'createdAt';
const IMyTable_isDeleted = 'isDeleted';

export const IMyTable_keys = [
  IMyTable_id,
  IMyTable_title,
  IMyTable_createdAt,
  IMyTable_isDeleted,
];

export interface IMyTable {
  [IMyTable_id]: number;
  [IMyTable_title]: string;
  [IMyTable_createdAt]: Date;
  [IMyTable_isDeleted]: boolean;
}

Уявіть, що у вас просто багато моделей, і робіть це для всіх ... це такий дорогий час.
— Вільям Куерво

-6
// declarations.d.ts
export interface IMyTable {
      id: number;
      title: string;
      createdAt: Date;
      isDeleted: boolean
}
declare var Tes: IMyTable;
// call in annother page
console.log(Tes.id);

1
Цей код не буде працювати, оскільки синтаксис машинопису недоступний під час виконання. Якщо ви перевірите цей код на дитячому майданчику машинопису, то ви помітите, що єдине, що компілюється в JavaScript, - console.log(Tes.id)це, звичайно, помилка "Uncaught ReferenceError: Tes is not defined"
— Тушар Шукла,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.