Создайте объект функции со свойствами в TypeScript

Я хочу создать функциональный объект, который также имеет некоторые свойства. Например, в JavaScript я бы сделал:

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

Теперь в TypeScript я могу описать этот тип как:

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

Однако я не могу построить его, не требуя гипса. Такие как:

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

Как бы вы построили это без гипса?


person JL235    schedule 07.10.2012    source источник
comment
Это не прямой ответ на ваш вопрос, но для тех, кто хочет лаконично построить объект функции со свойствами и согласен с приведением типов, Оператор Object-Spread, похоже, добивается цели: var f: { (): any; someValue: number; } = <{ (): any; someValue: number; }>{ ...(() => "Hello"), someValue: 3 };.   -  person Jonathan    schedule 29.11.2017


Ответы (9)


Итак, если требуется просто построить и присвоить эту функцию «f» без приведения, вот возможное решение:

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

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

По сути, он использует самоисполняющийся функциональный литерал для «конструирования» объекта, который будет соответствовать этой сигнатуре до того, как будет выполнено присвоение. Единственная странность заключается в том, что внутреннее объявление функции должно иметь тип 'any', иначе компилятор будет кричать, что вы назначаете свойство, которое еще не существует в объекте.

РЕДАКТИРОВАТЬ: немного упростил код.

person nxn    schedule 07.10.2012
comment
Насколько я понимаю, это на самом деле не проверяет тип, поэтому .someValue может быть практически любым. - person shabunc; 26.01.2015
comment
Лучший / обновленный ответ от 2017/2018 для TypeScript 2.0 опубликован Мейрионом Хьюзом ниже: stackoverflow.com/questions/12766528/. - person user3773048; 08.06.2018

Обновление: этот ответ был лучшим решением в более ранних версиях TypeScript, но в более новых версиях доступны лучшие варианты (см. Другие ответы).

Принятый ответ работает и может потребоваться в некоторых ситуациях, но имеет недостаток в том, что он не обеспечивает безопасность типов для создания объекта. Этот метод по крайней мере вызовет ошибку типа, если вы попытаетесь добавить неопределенное свойство.

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

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

// type error
f.notDeclard = 3
person Greg Weber    schedule 05.09.2013
comment
Я не понимаю, почему строка var f не вызывает ошибки, поскольку в то время нет свойства someValue. - person ; 14.07.2016
comment
@torazaburo это потому, что он не проверяет, когда вы бросаете. Чтобы ввести проверку, вам необходимо выполнить: var f:F = function(){}, что не поможет в приведенном выше примере. Этот ответ не подходит для более сложных ситуаций, поскольку вы теряете проверку типов на этапе присваивания. - person Meirion Hughes; 26.01.2017
comment
правильно, но как это сделать с помощью объявления функции вместо выражения функции? - person Alexander Mills; 02.04.2017
comment
Это больше не будет работать в последних версиях TS, потому что проверка интерфейса намного строже, и приведение функции больше не будет компилироваться из-за отсутствия необходимого свойства someValue. Что будет работать, так это <F><any>function() { ... } - person galenus; 27.07.2017
comment
когда вы используете tslint: рекомендуется, приведение типов не работает, и вам нужно использовать следующее: (заключите функцию в скобки, чтобы сделать ее выражением; затем используйте как любое, как F): var f = (function () {}) as any as F; - person NikhilWanpal; 14.11.2017
comment
Ключом к тому, чтобы это работало для меня, было то, что в интерфейсе должен присутствовать `(): any 'и не может быть возвращаемого значения. Таким образом, даже если функция возвращает число / строку / класс, вы не можете указать это - person Drenai; 21.12.2017

Теперь это легко достижимо (машинописный текст 2.x) с Object.assign(target, source)

пример:

введите описание изображения здесь

Волшебство здесь в том, что Object.assign<T, U>(t: T, u: U) набирается для возврата пересечения T & U.

Обеспечить, чтобы это разрешалось в известный интерфейс, также просто. Например:

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

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

какие ошибки из-за несовместимого набора текста:

Ошибка: foo: число не может быть присвоено foo: string
Ошибка: число не может быть присвоено строке [] (тип возвращаемого значения)

предостережение: вам может потребоваться polyfill Object.assign, если ориентирован на старые браузеры.

person Meirion Hughes    schedule 25.01.2017
comment
Отлично, спасибо. По состоянию на февраль 2017 года это лучший ответ (для пользователей Typescript 2.x). - person Andrew Faulkner; 22.02.2017
comment
По-прежнему кажется лучшим ответом и в 2021 году. - person Xunnamius; 03.06.2021

TypeScript разработан для обработки этого случая посредством слияния деклараций :

вы также можете быть знакомы с практикой JavaScript по созданию функции и последующему расширению функции путем добавления свойств в функцию. TypeScript использует объединение деклараций для создания подобных определений безопасным для типов способом.

Объединение деклараций позволяет нам сказать, что что-то является одновременно функцией и пространством имен (внутренним модулем):

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

Это сохраняет набор текста и позволяет писать как f(), так и f.someValue. При написании .d.ts файла для существующего кода JavaScript используйте declare:

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

Добавление свойств к функциям часто является запутанным или неожиданным шаблоном в TypeScript, поэтому старайтесь избегать этого, но это может быть необходимо при использовании или преобразовании старого кода JS. Это один из немногих случаев, когда было бы целесообразно смешивать внутренние модули (пространства имен) с внешними.

person mk.    schedule 28.10.2015
comment
Проголосуйте за упоминание внешних модулей в ответе. Это очень типичный случай при преобразовании или аннотировании существующих модулей JS. Именно то, что я ищу! - person Nipheris; 16.05.2016
comment
Прекрасный. Если вы хотите использовать это с модулями ES6, вы можете просто сделать function hello() { .. } namespace hello { export const value = 5; } export default hello; IMO, это намного чище, чем Object.assign или аналогичные хаки времени выполнения. Ни времени выполнения, ни утверждений типа, ничего. - person skrebbel; 26.07.2017
comment
Это замечательный ответ, но как прикрепить к функции символ, такой как Symbol.iterator? - person kzh; 28.07.2017
comment
Вы должны знать, что слияние деклараций приводит к довольно нестабильному JavaScript. Он добавляет дополнительное закрытие, которое мне кажется чрезмерным для простого присвоения свойств. Это очень не-JavaScript способ сделать это. Он также вводит проблему порядка зависимостей, при которой результирующее значение не обязательно является функцией, если вы не уверены, что функция - это первое, что нужно обязательно. - person John Leidegren; 27.08.2018
comment
Ding ding ding! Это правильный путь: нет необходимости в утверждениях и необходимости разделять объявление функции на 2 части, как в большинстве других ответов. - person fregante; 22.11.2020

Старый вопрос, но для версий TypeScript, начиная с 3.1, вы можете просто назначить свойство, как в обычном JS, если вы используете объявление функции или ключевое слово const для своей переменной:

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

Справочник и онлайн-пример.

person eritbh    schedule 26.11.2018

Не могу сказать, что это очень просто, но это определенно возможно:

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

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

если вам интересно, это из моей сути с TypeScript / JavaScript-версией Optional

person thiagoh    schedule 07.02.2018

В качестве ярлыка вы можете динамически назначать значение объекта с помощью метода доступа ['property']:

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

Это обходит проверку типа. Однако это довольно безопасно, потому что вы должны намеренно получить доступ к свойству таким же образом:

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

Однако, если вам действительно нужна проверка типа для значения свойства, это не сработает.

person Rick Love    schedule 11.10.2012
comment
Это уже работает? Не могу заставить его работать в моем файле .ts. - person Riza Khan; 15.05.2021
comment
Нет, это очень старое. Не делай этого. - person Rick Love; 19.05.2021

Обновленный ответ: после добавления типов пересечений через &, можно «на лету» «объединить» два предполагаемых типа.

Вот общий помощник, который считывает свойства некоторого объекта from и копирует их поверх объекта onto. Он возвращает тот же объект onto, но с новым типом, который включает оба набора свойств, поэтому правильно описывает поведение во время выполнения:

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

Этот низкоуровневый помощник по-прежнему выполняет утверждение типа, но по своей конструкции он безопасен для типов. С этим помощником у нас есть оператор, который мы можем использовать для решения проблемы OP с полной безопасностью типов:

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

Нажмите здесь чтобы опробовать его на площадке TypeScript Playground. Обратите внимание, что мы ограничили foo типом Foo, поэтому результат merge должен быть полным Foo. Поэтому, если вы переименуете bar в bad, вы получите ошибку типа.

NB Однако здесь есть еще одно типовое отверстие. TypeScript не позволяет ограничить параметр типа «не функцией». Так что вы можете запутаться и передать свою функцию в качестве второго аргумента merge, и это не сработает. Итак, пока это не будет объявлено, мы должны поймать его во время выполнения:

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;
}
person Daniel Earwicker    schedule 10.03.2016

Это отличается от строгой типизации, но вы можете

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

если вы пытаетесь обойти тяжелую типизацию, как я, когда нашел этот вопрос. К сожалению, это тот случай, когда TypeScript не работает на совершенно корректном JavaScript, поэтому вам нужно сказать TypeScript, чтобы он отступил.

«Ваш JavaScript является совершенно правильным TypeScript» оценивается как false. (Примечание: используется 0,95)

person WillSeitz    schedule 21.02.2014
comment
Это спасло меня, хотя я думаю, что это не лучший ответ. - person Riza Khan; 15.05.2021