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?


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.


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

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 .


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.


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


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.


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

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 .


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)

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.