Zbuduj obiekt funkcji z właściwościami w TypeScript

87

Chcę utworzyć obiekt funkcji, który również ma pewne właściwości. Na przykład w JavaScript zrobiłbym:

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

Teraz w TypeScript mogę opisać ten typ jako:

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

Jednak tak naprawdę nie mogę go zbudować bez konieczności rzucania. Jak na przykład:

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

Jak zbudowałbyś to bez obsady?

JL235
źródło
2
Nie jest to bezpośrednia odpowiedź na Twoje pytanie, ale dla każdego, kto chce zwięźle zbudować obiekt funkcyjny o właściwościach i jest OK z odlewu The operator obiektu-Spread wydaje rade: var f: { (): any; someValue: number; } = <{ (): any; someValue: number; }>{ ...(() => "Hello"), someValue: 3 };.
Jonathan

Odpowiedzi:

31

Więc jeśli wymaganiem jest po prostu zbudowanie i przypisanie tej funkcji do "f" bez rzutowania, oto możliwe rozwiązanie:

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

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

Zasadniczo używa samowykonującego się literału funkcji do „konstruowania” obiektu, który będzie pasował do tego podpisu przed wykonaniem przypisania. Jedyną dziwnością jest to, że wewnętrzna deklaracja funkcji musi być typu „any”, w przeciwnym razie kompilator wykrzykuje, że przypisujesz właściwość, która jeszcze nie istnieje w obiekcie.

EDYCJA: Uprościłem nieco kod.

nxn
źródło
5
O ile rozumiem, to faktycznie nie sprawdza typu, więc .someValue może oznaczać w zasadzie wszystko.
shabunc
1
Lepszą / zaktualizowaną odpowiedź z 2017/2018 na TypeScript 2.0 opublikował Meirion Hughes poniżej: stackoverflow.com/questions/12766528/… .
user3773048
108

Aktualizacja: ta odpowiedź była najlepszym rozwiązaniem we wcześniejszych wersjach TypeScript, ale są lepsze opcje dostępne w nowszych wersjach (zobacz inne odpowiedzi).

Zaakceptowana odpowiedź działa i może być wymagana w niektórych sytuacjach, ale ma tę wadę, że nie zapewnia bezpieczeństwa typu dla budowy obiektu. Ta technika przynajmniej zgłosi błąd typu, jeśli spróbujesz dodać niezdefiniowaną właściwość.

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

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

// type error
f.notDeclard = 3
Greg Weber
źródło
1
Nie rozumiem, dlaczego var flinia nie powoduje błędu, skoro w tym czasie nie ma someValuewłaściwości.
2
@torazaburo to dlatego, że nie sprawdza, kiedy rzucasz. Aby sprawdzić typ, musisz zrobić: var f:F = function(){}co zakończy się niepowodzeniem w powyższym przykładzie. Ta odpowiedź nie jest świetna w przypadku bardziej skomplikowanych sytuacji, ponieważ tracisz sprawdzanie typu na etapie przypisywania.
Meirion Hughes
1
prawda, ale jak to zrobić z deklaracją funkcji zamiast wyrażenia funkcji?
Alexander Mills
1
Nie będzie to już działać w ostatnich wersjach TS, ponieważ sprawdzanie interfejsu jest znacznie bardziej rygorystyczne, a rzutowanie funkcji nie będzie już kompilowane z powodu braku wymaganej właściwości „someValue”. Co jednak zadziała, to<F><any>function() { ... }
galenus
kiedy używasz tslint: zalecane, typecast nie działa, a to, czego potrzebujesz, to: (zawiń funkcję w nawiasy, aby uczynić ją wyrażeniem; następnie użyj jako dowolnego jako F):var f = (function () {}) as any as F;
NikhilWanpal
80

Jest to teraz łatwo osiągalne (maszynopis 2.x) z Object.assign(target, source)

przykład:

wprowadź opis obrazu tutaj

Magia polega na tym, że Object.assign<T, U>(t: T, u: U)jest wpisywany w celu zwrócenia przecięcia T & U .

Wymuszenie, że rozwiąże to problem na znanym interfejsie, jest również proste. Na przykład:

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

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

które błędy spowodowane niezgodnym pisaniem:

Błąd: foo: nie można przypisać numeru do foo: ciąg
Błąd: nie można przypisać numeru do ciągu [] (zwracany typ)

Uwaga: w przypadku kierowania na starsze przeglądarki może być konieczne wypełnienie kodu Object. assign .

Meirion Hughes
źródło
1
Fantastycznie, dziękuję. Od lutego 2017 r. Jest to najlepsza odpowiedź (dla użytkowników programu Typescript 2.x).
Andrew Faulkner
47

TypeScript jest przeznaczony do obsługi tego przypadku poprzez scalanie deklaracji :

możesz również znać praktykę JavaScript polegającą na tworzeniu funkcji, a następnie rozszerzaniu funkcji poprzez dodawanie właściwości do funkcji. TypeScript używa scalania deklaracji do tworzenia takich definicji w sposób bezpieczny dla typów.

Łączenie deklaracji pozwala nam powiedzieć, że coś jest zarówno funkcją, jak i przestrzenią nazw (moduł wewnętrzny):

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

To zachowuje pisanie i pozwala nam pisać zarówno f()i f.someValue. Podczas pisania .d.tspliku dla istniejącego kodu JavaScript użyj declare:

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

Dodawanie właściwości do funkcji jest często mylącym lub nieoczekiwanym wzorcem w TypeScript, więc staraj się tego unikać, ale może być konieczne podczas używania lub konwertowania starszego kodu JS. To jeden z nielicznych przypadków, w których należałoby mieszać moduły wewnętrzne (przestrzenie nazw) z zewnętrznymi.

mk.
źródło
Głosuj za wspomnieniem modułów otoczenia w odpowiedzi. Jest to bardzo typowy przypadek podczas konwersji lub adnotacji istniejących modułów JS. Dokładnie to, czego szukam!
Nipheris
Śliczny. Jeśli chcesz używać tego z modułami ES6, możesz po prostu zrobić function hello() { .. } namespace hello { export const value = 5; } export default hello;IMO, jest to znacznie czystsze niż Object. assign lub podobne hacki uruchomieniowe. Bez środowiska uruchomieniowego, bez asercji typu, bez niczego.
skrebbel
Ta odpowiedź jest świetna, ale jak dołączyć symbol, taki jak Symbol.iterator do funkcji?
kzh
Powyższy link nie działa dłużej, poprawne byłoby typescriptlang.org/docs/handbook/declaration-merging.html
iJungleBoy
Powinieneś wiedzieć, że łączenie deklaracji skutkuje powstaniem dość dziwnego JavaScript. Dodaje dodatkowe zamknięcie, które wydaje mi się przesadne w przypadku prostego przypisania właściwości. Jest to bardzo nie-JavaScriptowy sposób na zrobienie tego. Wprowadza również problem z kolejnością zależności, w którym wynikowa wartość niekoniecznie jest funkcją, chyba że masz pewność, że funkcja jest pierwszą wymaganą .
John Leidegren
3

Nie mogę powiedzieć, że jest to bardzo proste, ale na pewno jest możliwe:

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

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

jeśli zaciekawiło Cię to z mojej istoty w wersji TypeScript / JavaScript programuOptional

thiagoh
źródło
2

Jako skrót możesz dynamicznie przypisać wartość obiektu za pomocą metody dostępu ['property']:

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

To omija sprawdzanie typu. Jest to jednak dość bezpieczne, ponieważ musisz celowo uzyskać dostęp do nieruchomości w ten sam sposób:

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

Jeśli jednak naprawdę chcesz, aby typ sprawdzał wartość właściwości, to nie zadziała.

Rick Love
źródło
1

Zaktualizowana odpowiedź: od czasu dodania typów skrzyżowań poprzez &, możliwe jest „scalenie” dwóch wywnioskowanych typów w locie.

Oto ogólny pomocnik, który odczytuje właściwości jakiegoś obiektu fromi kopiuje je na obiekt onto. Zwraca ten sam obiekt, ontoale z nowym typem, który zawiera oba zestawy właściwości, więc poprawnie opisuje zachowanie w czasie wykonywania:

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

Ten pomocnik niskiego poziomu nadal wykonuje asercję typu, ale zgodnie z projektem jest bezpieczny dla typu. Mając tego pomocnika, mamy operatora, którego możemy użyć do rozwiązania problemu OP z pełnym bezpieczeństwem typu:

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

Kliknij tutaj, aby wypróbować to w TypeScript Playground . Zauważ, że ograniczyliśmy się foodo typu Foo, więc wynik mergemusi być kompletny Foo. Więc jeśli zmienisz nazwę barna bad, pojawi się błąd typu.

Uwaga: jest tu jednak jeszcze jeden rodzaj otworu. TypeScript nie zapewnia sposobu na ograniczenie parametru typu, aby nie był on funkcją. Więc możesz się pogubić i przekazać swoją funkcję jako drugi argument merge, a to nie zadziała. Więc dopóki nie zostanie to zadeklarowane, musimy to złapać w czasie wykonywania:

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
źródło
1

Stare pytanie, ale w przypadku wersji TypeScript zaczynających się od 3.1 możesz po prostu przypisać właściwość tak, jak w zwykłym JS, o ile używasz deklaracji funkcji lub constsłowa kluczowego dla swojej zmiennej:

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

Odniesienie i przykład online .

Geo1088
źródło
0

To odbiega od silnego pisania, ale możesz to zrobić

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

jeśli próbujesz obejść uciążliwe, mocne pisanie, tak jak ja, kiedy znalazłem to pytanie. Niestety jest to przypadek, w którym TypeScript nie działa na doskonale poprawnym JavaScript, więc musisz powiedzieć TypeScript, aby się wycofał.

„Twój JavaScript jest całkowicie poprawny TypeScript” przyjmuje wartość fałsz. (Uwaga: przy użyciu 0,95)

WillSeitz
źródło