Отримати ключі інтерфейсу 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.