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.