Erstellen Sie ein Funktionsobjekt mit Eigenschaften in TypeScript

83

Ich möchte ein Funktionsobjekt erstellen, auf dem auch einige Eigenschaften gespeichert sind. Zum Beispiel in JavaScript würde ich tun:

var f = function() { }
f.someValue = 3;

Jetzt kann ich in TypeScript den Typ wie folgt beschreiben:

var f: { (): any; someValue: number; };

Ich kann es jedoch nicht bauen, ohne eine Besetzung zu benötigen. Sowie:

var f: { (): any; someValue: number; } =
    <{ (): any; someValue: number; }>(
        function() { }
    );
f.someValue = 3;

Wie würden Sie dies ohne eine Besetzung bauen?

JL235
quelle
2
Dies ist keine direkte Antwort auf Ihre Frage, aber für alle, die ein Funktionsobjekt mit Eigenschaften präzise erstellen möchten und mit dem Casting einverstanden sind, scheint der Object-Spread-Operator den Trick zu tun : var f: { (): any; someValue: number; } = <{ (): any; someValue: number; }>{ ...(() => "Hello"), someValue: 3 };.
Jonathan

Antworten:

31

Wenn die Anforderung also darin besteht, diese Funktion einfach ohne Besetzung zu erstellen und "f" zuzuweisen, ist hier eine mögliche Lösung:

var f: { (): any; someValue: number; };

f = (() => {
    var _f : any = function () { };
    _f.someValue = 3;
    return _f;
})();

Im Wesentlichen wird ein selbstausführendes Funktionsliteral verwendet, um ein Objekt zu "konstruieren", das mit dieser Signatur übereinstimmt, bevor die Zuweisung abgeschlossen ist. Die einzige Seltsamkeit ist, dass die innere Deklaration der Funktion vom Typ 'any' sein muss, andernfalls schreit der Compiler, dass Sie einer Eigenschaft zuweisen, die für das Objekt noch nicht vorhanden ist.

EDIT: Den Code etwas vereinfacht.

nxn
quelle
5
Soweit ich verstehen kann, wird der Typ nicht überprüft, daher kann .someValue im Wesentlichen alles sein.
Shabunc
1
Die bessere / aktualisierte Antwort ab 2017/2018 für TypeScript 2.0 wird von Meirion Hughes unten veröffentlicht: stackoverflow.com/questions/12766528/… .
user3773048
107

Update: Diese Antwort war die beste Lösung in früheren Versionen von TypeScript, aber in neueren Versionen stehen bessere Optionen zur Verfügung (siehe andere Antworten).

Die akzeptierte Antwort funktioniert und kann in einigen Situationen erforderlich sein, hat jedoch den Nachteil, dass keine Typensicherheit für den Aufbau des Objekts bereitgestellt wird. Diese Technik löst zumindest einen Typfehler aus, wenn Sie versuchen, eine undefinierte Eigenschaft hinzuzufügen.

interface F { (): any; someValue: number; }

var f = <F>function () { }
f.someValue = 3

// type error
f.notDeclard = 3
Greg Weber
quelle
1
Ich verstehe nicht, warum die var fZeile keinen Fehler verursacht, da zu diesem Zeitpunkt keine someValueEigenschaft vorhanden ist.
2
@torazaburo ist, weil es nicht überprüft, wann Sie besetzen. Um die Prüfung einzugeben, müssen Sie Folgendes tun: var f:F = function(){}Dies schlägt im obigen Beispiel fehl. Diese Antwort eignet sich nicht für kompliziertere Situationen, da Sie die Typprüfung in der Zuweisungsphase verlieren.
Meirion Hughes
1
richtig, aber wie macht man das mit einer Funktionsdeklaration anstelle eines Funktionsausdrucks?
Alexander Mills
1
Dies funktioniert in den neueren Versionen von TS nicht mehr, da die Schnittstellenprüfung viel strenger ist und das Casting der Funktion aufgrund des Fehlens der erforderlichen Eigenschaft 'someValue' nicht mehr kompiliert werden kann. Was jedoch funktionieren wird, ist<F><any>function() { ... }
Galenus
Wenn Sie tslint verwenden: empfohlen, funktioniert typecast nicht und Sie müssen Folgendes verwenden: (Umschließen Sie die Funktion in Klammern, um sie zu einem Ausdruck zu machen; verwenden Sie sie dann als F):var f = (function () {}) as any as F;
NikhilWanpal
77

Dies ist jetzt leicht erreichbar (Typoskript 2.x) mit Object.assign(target, source)

Beispiel:

Geben Sie hier die Bildbeschreibung ein

Die Magie hier ist, dass Object.assign<T, U>(t: T, u: U)eingegeben wird, um die Kreuzung zurückzugeben T & U .

Die Durchsetzung, dass dies in eine bekannte Schnittstelle aufgelöst wird, ist ebenfalls unkompliziert. Zum Beispiel:

interface Foo {
  (a: number, b: string): string[];
  foo: string;
}

let method: Foo = Object.assign(
  (a: number, b: string) => { return a * a; },
  { foo: 10 }
); 

welche Fehler aufgrund inkompatibler Eingabe:

Fehler: foo: Nummer nicht foo: string zuweisbar
Fehler: Nummer nicht string [] zuweisbar (Rückgabetyp)

Nachteil: Sie müssen möglicherweise POLYfill Object.assign wenn älteren Browser - Targeting.

Meirion Hughes
quelle
1
Fantastisch, danke. Ab Februar 2017 ist dies die beste Antwort (für Benutzer von Typescript 2.x).
Andrew Faulkner
Jetzt ist es 2020, gibt es aktualisierte Lösungen?
Archsx
46

TypeScript wurde entwickelt, um diesen Fall durch Zusammenführen von Deklarationen zu behandeln :

Möglicherweise sind Sie auch mit der JavaScript-Praxis vertraut, eine Funktion zu erstellen und die Funktion dann weiter zu erweitern, indem Sie der Funktion Eigenschaften hinzufügen. TypeScript verwendet das Zusammenführen von Deklarationen, um Definitionen wie diese typsicher aufzubauen.

Durch das Zusammenführen von Deklarationen können wir sagen, dass etwas sowohl eine Funktion als auch ein Namespace ist (internes Modul):

function f() { }
namespace f {
    export var someValue = 3;
}

Dadurch bleibt die Eingabe erhalten, und wir können sowohl f()als auch schreiben f.someValue. .d.tsVerwenden Sie beim Schreiben einer Datei für vorhandenen JavaScript-Code Folgendes declare:

declare function f(): void;
declare namespace f {
    export var someValue: number;
}

Das Hinzufügen von Eigenschaften zu Funktionen ist in TypeScript häufig ein verwirrendes oder unerwartetes Muster. Versuchen Sie daher, es zu vermeiden. Dies kann jedoch erforderlich sein, wenn Sie älteren JS-Code verwenden oder konvertieren. Dies ist eines der wenigen Male, bei denen es angebracht wäre, interne Module (Namespaces) mit externen zu mischen.

mk.
quelle
Upvote für die Erwähnung von Umgebungsmodulen in der Antwort. Dies ist ein sehr typischer Fall beim Konvertieren oder Kommentieren vorhandener JS-Module. Genau das, wonach ich suche!
Nipheris
Schön. Wenn Sie dies mit ES6-Modulen verwenden möchten, können Sie einfach function hello() { .. } namespace hello { export const value = 5; } export default hello;IMO ausführen. Dies ist viel sauberer als Object.assign oder ähnliche Laufzeit-Hacks. Keine Laufzeit, keine Typzusicherungen, kein Nichts.
Skrebbel
Diese Antwort ist großartig, aber wie hängt man ein Symbol wie Symbol.iterator an eine Funktion an?
kzh
Der obige Link funktioniert nicht mehr, richtig wäre typescriptlang.org/docs/handbook/declaration-merging.html
iJungleBoy
Sie sollten wissen, dass das Zusammenführen von Deklarationen zu ziemlich wackeligem JavaScript führt. Es wird eine zusätzliche Schließung hinzugefügt, die mir für eine einfache Immobilienzuweisung übertrieben erscheint. Es ist eine sehr un-JavaScript-Methode. Außerdem wird ein Problem mit der Reihenfolge der Abhängigkeiten eingeführt, bei dem der resultierende Wert nicht unbedingt eine Funktion ist, es sei denn, Sie sind sicher, dass die Funktion als erstes erforderlich ist .
John Leidegren
3

Ich kann nicht sagen, dass es sehr einfach ist, aber es ist definitiv möglich:

interface Optional {
  <T>(value?: T): OptionalMonad<T>;
  empty(): OptionalMonad<any>;
}

const Optional = (<T>(value?: T) => OptionalCreator(value)) as Optional;
Optional.empty = () => OptionalCreator();

Wenn Sie neugierig geworden sind, ist dies ein Kernstück von mir mit der TypeScript / JavaScript-Version vonOptional

Thiagoh
quelle
Jetzt ist es 2020, gibt es aktualisierte Lösungen?
Archsx
2

Als Verknüpfung können Sie den Objektwert mithilfe des Accessors ['Eigenschaft'] dynamisch zuweisen:

var f = function() { }
f['someValue'] = 3;

Dies umgeht die Typprüfung. Es ist jedoch ziemlich sicher, da Sie absichtlich auf die gleiche Weise auf die Immobilie zugreifen müssen:

var val = f.someValue; // This won't work
var val = f['someValue']; // Yeah, I meant to do that

Wenn Sie jedoch wirklich möchten, dass der Typ den Eigenschaftswert überprüft, funktioniert dies nicht.

Rick Love
quelle
1

Eine aktualisierte Antwort: seit dem Hinzufügen von Kreuzungstypen über & ist es möglich, zwei abgeleitete Typen im laufenden Betrieb "zusammenzuführen".

Hier ist ein allgemeiner Helfer, der die Eigenschaften eines Objekts liest fromund über ein Objekt kopiert onto. Es gibt dasselbe Objekt zurück, ontojedoch mit einem neuen Typ, der beide Eigenschaftensätze enthält, sodass das Laufzeitverhalten korrekt beschrieben wird:

function merge<T1, T2>(onto: T1, from: T2): T1 & T2 {
    Object.keys(from).forEach(key => onto[key] = from[key]);
    return onto as T1 & T2;
}

Dieser Low-Level-Helfer führt zwar noch eine Typzusicherung durch, ist jedoch von Natur aus typsicher. Mit diesem Helfer haben wir einen Bediener, mit dem wir das OP-Problem mit voller Sicherheit lösen können:

interface Foo {
    (message: string): void;
    bar(count: number): void;
}

const foo: Foo = merge(
    (message: string) => console.log(`message is ${message}`), {
        bar(count: number) {
            console.log(`bar was passed ${count}`)
        }
    }
);

Klicken Sie hier, um es auf dem TypeScript-Spielplatz auszuprobieren . Beachten Sie, dass wir uns darauf beschränkt haben foo, vom Typ zu sein Foo, daher muss das Ergebnis von mergevollständig sein Foo. Also, wenn Sie umbenennen barinbad Sie einen Tippfehler.

Hinweis: Hier gibt es jedoch noch ein Loch. TypeScript bietet keine Möglichkeit, einen Typparameter auf "keine Funktion" zu beschränken. Sie könnten also verwirrt sein und Ihre Funktion als zweites Argument an übergeben merge, und das würde nicht funktionieren. Bis dies deklariert werden kann, müssen wir es zur Laufzeit abfangen:

function merge<T1, T2>(onto: T1, from: T2): T1 & T2 {
    if (typeof from !== "object" || from instanceof Array) {
        throw new Error("merge: 'from' must be an ordinary object");
    }
    Object.keys(from).forEach(key => onto[key] = from[key]);
    return onto as T1 & T2;
}
Daniel Earwicker
quelle
1

Alte Frage, aber für Versionen von TypeScript ab 3.1 können Sie die Eigenschaftszuweisung einfach wie in einfachem JS durchführen, solange Sie eine Funktionsdeklaration oder das constSchlüsselwort für Ihre Variable verwenden:

function f () {}
f.someValue = 3; // fine
const g = function () {};
g.someValue = 3; // also fine
var h = function () {};
h.someValue = 3; // Error: "Property 'someValue' does not exist on type '() => void'"

Referenz und Online-Beispiel .

Geo1088
quelle
0

Dies weicht von starker Eingabe ab, aber Sie können dies tun

var f: any = function() { }
f.someValue = 3;

Wenn Sie versuchen, drückend starkes Tippen zu umgehen, wie ich es war, als ich diese Frage fand. Leider ist dies ein Fall, in dem TypeScript bei einwandfreiem JavaScript fehlschlägt. Sie müssen TypeScript daher anweisen, sich zurückzuziehen.

"Ihr JavaScript ist vollkommen gültiges TypeScript" wird mit false bewertet. (Hinweis: mit 0,95)

WillSeitz
quelle