Pobierz klucze interfejsu Typescript jako tablicę ciągów


113

Mam wiele tabel w Lovefield i odpowiadających im interfejsów dla posiadanych kolumn.
Przykład:

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

Chciałbym mieć nazwy właściwości tego interfejsu w tablicy takiej jak ta:

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

nie mogę utworzyć obiektu / tablicy IMyTablebezpośrednio na podstawie interfejsu, co powinno załatwić sprawę, ponieważ dynamicznie otrzymywałbym nazwy interfejsów tabel. Dlatego muszę iterować te właściwości w interfejsie i uzyskać z niego tablicę.

Jak osiągnąć ten wynik?

Odpowiedzi:


57

Od TypeScript 2.3 (lub powinienem powiedzieć 2.4 , ponieważ w 2.3 ta funkcja zawiera błąd, który został naprawiony w typescript@2.4-dev ), możesz utworzyć niestandardowy transformator, aby osiągnąć to, co chcesz zrobić.

Właściwie już stworzyłem taki niestandardowy transformator, który umożliwia co następuje.

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']

Niestety niestandardowe transformatory nie są obecnie tak łatwe w użyciu. Musisz ich używać z interfejsem API transformacji TypeScript zamiast wykonywać polecenie tsc. Wystąpił problem z żądaniem obsługi wtyczki dla niestandardowych transformatorów.


Dziękuję za odpowiedź, już wczoraj widziałem i zainstalowałem ten niestandardowy transformator, ale ponieważ używa on maszynopisu 2.4, na razie nie jest to dla mnie przydatne.
— Tushar Shukla

17
Cześć, ta biblioteka również spełnia dokładnie moje wymagania, jednak otrzymuję, ts_transformer_keys_1.keys is not a functiongdy postępuję zgodnie z dokładnymi krokami w dokumentacji. czy istnieje obejście tego problemu?
— Hasitha Shan

Schludny! Czy uważasz, że można go rozszerzyć, aby przyjmował parametr typu dynamicznego (uwaga 2 w pliku Readme)?
— kenshin

@HasithaShan przyjrzyj się dokładniej dokumentom - musisz użyć API kompilatora TypeScript, aby pakiet działał
— Yaroslav Bai

3
Niestety paczka jest zepsuta, cokolwiek zrobię, zawsze dostajęts_transformer_keys_1.keys is not a function
— fr1sk

20

Poniższa wymaga notować klucze na własną rękę, ale przynajmniej maszynopis wymusi IUserProfilei IUserProfileKeysmają te same klucze ( Required<T>dodano w maszynopisie 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,
};

Całkiem fajna sztuczka. Teraz łatwo jest wymusić implementację wszystkich kluczy IUserProfilei byłoby łatwo wyodrębnić je z const IUserProfileKeys. To jest dokładnie to, czego szukałem. Nie ma teraz potrzeby konwertowania wszystkich moich interfejsów na klasy.
— Anddo

17

Miałem podobny problem, że miałem gigantyczną listę właściwości, które chciałem mieć zarówno interfejs, jak i obiekt z niego.

UWAGA: Nie chciałem pisać (wpisywać za pomocą klawiatury) właściwości dwa razy! Po prostu SUCHY.


Należy tu zauważyć, że interfejsy są wymuszonymi typami w czasie kompilacji, podczas gdy obiekty są głównie w czasie wykonywania. ( Źródło )

Jak @derek wspomniał w innej odpowiedzi , wspólnym mianownikiem interfejsu i obiektu może być klasa, która obsługuje zarówno typ, jak i wartość .

Tak więc, TL; DR, następujący fragment kodu powinien zaspokoić potrzeby:

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,
};

( Oto powyższy kod w Typescript Playground, aby odtworzyć więcej)

PS. Jeśli nie chcesz przypisywać początkowych wartości właściwościom w klasie i pozostać przy typie, możesz zrobić sztuczkę konstruktora:

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"] 

Sztuczka konstruktora w zabawie TypeScript .


Jest propsArrayon jednak dostępny tylko wtedy, gdy zainicjowałeś klucze.
— denkquer

Nie rozumiem, co masz na myśli przez „zainicjowany” @denkquer. W pierwszym przykładzie propsArrayjest dostępny przed tableInstanceif o tym, co masz na myśli, więc wyraźnie przed inicjalizacją instancji. Jeśli jednak odnosisz się do fałszywych wartości przypisanych w programie MyTableClass, to one mają na celu pokrótce zasugerować „typ” właściwości. Jeśli ich nie chcesz, możesz skorzystać ze sztuczki konstruktora w przykładzie PS.
— Aidin

1
W moim rozumieniu wartość jest inicjowana, gdy ma jakąkolwiek wartość. Twoja „sztuczka konstruktora” jest myląca, ponieważ nie możesz po prostu zastąpić tej MyTableClassdrugiej i oczekiwać, że otrzymasz klucze w propsArrayniezainicjowanych zmiennych, a typy są usuwane w czasie wykonywania. Zawsze musisz podać im jakąś domyślną wartość. Przekonałem się, że zainicjowanie ich za pomocą undefinedjest najlepszym podejściem.
— denkquer

1
@Aidin dziękuję za rozwiązanie. Zastanawiam się też, czy mogę uniknąć inicjalizacji parametrów. Jeśli użyję sztuczki konstruktora, nie mogę już utworzyć interfejsu, który rozszerza MyTableClass .. Twoja sztuczka konstruktora w maszynie jest jednak pusta
— Flion

1
@Flion, dzięki, że to zauważyłeś. Właśnie zaktualizowałem link do placu zabaw dla sztuczki konstruktora. Zobacz, czy teraz działa.
— Aidin

11

Może jest już za późno, ale w wersji 2.1 maszynopisu możesz użyć w key often sposób:

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

Dokument: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html#keyof-and-lookup-types


Dzięki za odpowiedź, ale nie jestem pewien, czy pomaga to komuś w użyciu statycznie utworzonych typów z interfejsu. IMHO, w większości przypadków możemy używać zamiennie interfejsów / typów. Ponadto wymagałoby to ręcznego tworzenia typów dla wielu interfejsów. Jednak rozwiązanie wygląda dobrze, jeśli ktoś potrzebuje tylko wyrzucić typy z interfejsu.
— Tushar Shukla

9

To powinno działać

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

lub

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

17
Nie żeby to było złe, ale żeby było jasne, po prostu „wymuszasz poprawność wartości tablicy”. Deweloper nadal musi zapisać je dwukrotnie, ręcznie.
— Aidin

Chociaż to, co powiedział Aidin, mogło być prawdą, w niektórych przypadkach właśnie tego szukałem w moim przypadku. Dziękuję Ci.
— Daniel

4
Nie zapobiegnie to duplikowaniu kluczy lub brakującym kluczom. Na przykładvar IMyTable: Array<keyof IMyTable> = ["id", "createdAt", "id"];
— ford04

Dla mnie również tego szukałem, ponieważ chcę opcjonalnie zaakceptować klucze, ale nic więcej niż klucze zdefiniowane w interfejsie. Nie spodziewałem się, że będzie to domyślne z powyższym kodem. Myślę, że nadal potrzebowalibyśmy do tego wspólnego sposobu na TS. W każdym razie dziękuję za powyższy kod!
— nicoes

8

Bezpieczne warianty

Tworzenie tablicy lub krotki kluczy z poziomu interfejsu z kontrolą bezpieczeństwa w czasie kompilacji wymaga trochę kreatywności. Typy są usuwane w czasie wykonywania, a typy obiektów (nieuporządkowane, nazwane) nie mogą być konwertowane na typy krotek (uporządkowane, bez nazw) bez uciekania się do nieobsługiwanych technik .

Porównanie z innymi odpowiedziami

Wszystkie proponowane warianty uwzględniają / wywołują błąd kompilacji w przypadku zduplikowanych lub brakujących elementów krotki, biorąc pod uwagę typ obiektu odniesienia, taki jak IMyTable. Na przykład zadeklarowanie typu tablicy (keyof IMyTable)[]nie może wychwycić tych błędów.

Ponadto nie wymagają konkretnej biblioteki (ostatnie zastosowania wariantu ts-morph, które rozważałbym jako ogólny wrapper kompilatora), emitują typ krotki w przeciwieństwie do obiektu (tylko pierwsze rozwiązanie tworzy tablicę) lub typ szerokiej tablicy (w porównaniu do te odpowiedzi ) i wreszcie nie potrzebują zajęć .

Wariant 1: prosta tablica typu

// 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")[]

+najłatwiejszy +-podręcznik z -tablicą autouzupełniania , bez krotki

Plac zabaw

Jeśli nie lubisz tworzyć rekordów, spójrz na tę alternatywę z Settypami asercji i .


Wariant 2: krotka z funkcją pomocniczą

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

++-instrukcja krotki z automatycznym uzupełnianiem, +-bardziej zaawansowane, złożone typy

Plac zabaw

Wyjaśnienie

createKeyswykonuje kontrole w czasie kompilacji, łącząc typ parametru funkcji z dodatkowymi typami asercji, które generują błąd dla nieodpowiednich danych wejściowych. (keyof IMyTable)[] | [keyof IMyTable]jest "czarną magią" sposobem na wymuszenie wnioskowania krotki zamiast tablicy po stronie wywoływanej. Alternatywnie możesz użyć potwierdzenia const /as const po stronie wywołującego.

CheckMissingsprawdza, jeśli Tbrakuje kluczy z 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"]

Uwaga: T & "Error: missing keys"dotyczy tylko ładnych błędów IDE. Możesz też pisać never. CheckDuplicatessprawdza elementy z podwójną krotką:

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"]]

Uwaga: Więcej informacji na temat sprawdzania unikalnych przedmiotów w krotkach znajduje się w tym poście . Dzięki TS 4.1 możemy również nazwać brakujące klucze w ciągu błędu - spójrz na ten Playground .


Wariant 3: typ rekurencyjny

W wersji 4.1 TypeScript oficjalnie obsługuje warunkowe typy rekurencyjne , które mogą być tutaj również potencjalnie użyte. Chociaż obliczenia typu są kosztowne ze względu na złożoność kombinacyjną - wydajność znacznie spada dla więcej niż 5-6 pozycji. Podaję tę alternatywę dla kompletności ( Playground ):

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"] // ✔

++-instrukcja krotki z automatycznym uzupełnianiem +brak --działania funkcji pomocniczej


Wariant 4: Generator kodu / API kompilatora TS

Tutaj wybrano ts-morph , ponieważ jest to odrobinę prostsza alternatywa opakowania dla oryginalnego API kompilatora TS . Oczywiście możesz również bezpośrednio użyć interfejsu API kompilatora. Spójrzmy na kod generatora:

// ./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

Po skompilowaniu i uruchomieniu tego pliku tsc && node dist/mybuildstep.js, ./src/generated/IMyTable-keys.tsgenerowany jest plik o następującej zawartości:

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

+rozwiązanie automatycznie generujące +skalowalne dla wielu właściwości +żadna funkcja pomocnicza +krotka -dodatkowy krok kompilacji -wymaga znajomości interfejsu API kompilatora


6

Zamiast definiować IMyTablejak w interfejsie, spróbuj zdefiniować go jako klasę. W maszynopisie możesz użyć klasy takiej jak interfejs.

Na przykład zdefiniuj / wygeneruj swoją klasę w następujący sposób:

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

Użyj go jako interfejsu:

export class SomeTable implements IMyTable {
    ...
}

Zdobądź klucze:

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

5

Będziesz musiał utworzyć klasę, która implementuje twój interfejs, utworzyć jej instancję, a następnie użyć jej Object.keys(yourObject)do pobrania właściwości.

export class YourClass implements IMyTable {
    ...
}

następnie

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

Nie działa w moim przypadku, musiałbym wymienić te właściwości interfejsu, ale nie tego chcę? Nazwa interfejsu pojawia się dynamicznie, a następnie muszę określić jego właściwości
— Tushar Shukla

Powoduje to błąd (v2.8.3): Cannot extend an interface […]. Did you mean 'implements'?Jednak używanie implementszamiast tego wymaga ręcznego skopiowania interfejsu, co jest dokładnie tym, czego nie chcę.
— jacob

@jacob przepraszam, powinno być implementsi zaktualizowałem moją odpowiedź. Jak stwierdził @basarat, interfejsy nie istnieją w czasie wykonywania, więc jedynym sposobem jest zaimplementowanie go jako klasy.
— Dan Def

Masz na myśli zamiast interfejsu użyć klasy? Niestety nie mogę, ponieważ interfejs pochodzi od strony trzeciej ( @types/react). Skopiowałem je ręcznie, ale nie jest to przyszłościowe 😪 Próbuję dynamicznie wiązać metody nieobjęte cyklem życia (które są już powiązane), ale nie są one zadeklarowane w React.Component (klasie).
— jacob

Nie, mam na myśli utworzenie klasy, która implementuje interfejs innej firmy i uzyskanie właściwości tej klasy w czasie wykonywania.
— Dan Def

3

Żargon. Interfejsy nie istnieją w czasie wykonywania.

obejście

Utwórz zmienną tego typu i użyj Object.keysna niej 🌹


1
Czy masz na myśli tak:var abc: IMyTable = {}; Object.keys(abc).forEach((key) => {console.log(key)});
— Tushar Shukla

4
Nie, ponieważ ten obiekt nie ma kluczy. Interfejs to coś, czego używa TypeScript, ale wyparowuje w JavaScript, więc nie ma żadnych informacji, które mogłyby określić jakiekolwiek „odbicie” lub „przecięcie”. JavaScript wie tylko, że istnieje pusty literał obiektu. Jedyną nadzieją jest poczekanie na (lub żądanie ) TypeScript zawiera sposób na wygenerowanie tablicy lub obiektu ze wszystkimi kluczami w interfejsie do kodu źródłowego. Lub, jak mówi Dan Def, jeśli możesz użyć klasy, będziesz mieć klucze zdefiniowane w postaci właściwości w każdym przypadku ..
— Jesper

17
Jeśli to nie zadziała, dlaczego są głosy za tą odpowiedzią?
— dawez

2
powód głosowania w dół: nie ma wzmianki, że nie działa dla wartości
— zerowych

2
Ostatecznie nie jest to świetne rozwiązanie, ponieważ musisz dostarczać wartości. Prawdopodobnie lepiej jest po prostu zachować listę kluczy.
— Daniel Thompson,

0

To było trudne! Dziękuję wszystkim za pomoc.

Moją potrzebą było uzyskanie kluczy interfejsu jako tablicy ciągów w celu uproszczenia skryptów mocha / chai. Nie martwię się o używanie w aplikacji (jeszcze), więc nie trzeba było tworzyć plików ts. Dzięki ford04 za pomoc, jego rozwiązanie powyżej było ogromną pomocą i działa idealnie, BEZ hacków kompilatora. Oto zmodyfikowany kod:

Opcja 2: Generator kodu oparty na API kompilatora TS (ts-morph)

Moduł węzła

npm install --save-dev ts-morph

keys.ts

UWAGA : zakłada się, że wszystkie pliki ts znajdują się w katalogu głównym ./src i nie ma podfolderów, dostosuj odpowiednio

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;

stosowanie

import keys from "./keys";

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

console.log(myKeys)

-1

Nie możesz tego zrobić. Interfejsy nie istnieją w czasie wykonywania (jak powiedział @basarat ).

Teraz pracuję z następującymi elementami:

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;
}

Wyobraź sobie, że masz wiele modeli i zrób to dla każdego ... to taki drogi czas.
— William Cuervo

-7
// 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
Ten kod nie będzie działał, ponieważ składnia skryptu nie jest dostępna w czasie wykonywania. Jeśli sprawdzisz ten kod na maszynie, zauważysz, że jedyną rzeczą, która kompiluje się do JavaScript, jest console.log(Tes.id)oczywiście błąd „Uncaught ReferenceError: Tes is not defined”
— Tushar Shukla
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.