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

87

Я хочу создать функциональный объект, который также имеет некоторые свойства. Например, в 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;

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

JL235
источник
2
Это не является прямым ответом на ваш вопрос, но и для тех , кто хочет лаконично построить функциональный объект со свойствами, и все в порядке с литьем, то оператор Object-Spread , кажется, сделать трюк: var f: { (): any; someValue: number; } = <{ (): any; someValue: number; }>{ ...(() => "Hello"), someValue: 3 };.
Джонатан

Ответы:

31

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

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

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

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

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

nxn
источник
5
Насколько я понимаю, это фактически не проверяет тип, поэтому .someValue может быть практически любым.
shabunc
1
Лучший / обновленный ответ от 2017/2018 для TypeScript 2.0 опубликован Мейрионом Хьюзом ниже: stackoverflow.com/questions/12766528/… .
user3773048 07
108

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

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

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

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

// type error
f.notDeclard = 3
Грег Вебер
источник
1
Не понимаю, почему var fстрока не вызывает ошибку, ведь на тот момент someValueсвойства нет .
2
@torazaburo это потому, что он не проверяет, когда вы бросаете. Чтобы ввести проверку, вам нужно сделать: var f:F = function(){}что не удастся в приведенном выше примере. Этот ответ не подходит для более сложных ситуаций, поскольку вы теряете проверку типов на этапе назначения.
Мейрион Хьюз
1
правильно, но как это сделать с помощью объявления функции вместо выражения функции?
Александр Миллс
1
Это больше не будет работать в последних версиях TS, потому что проверка интерфейса намного строже, а приведение функций больше не будет компилироваться из-за отсутствия необходимого свойства someValue. Но что сработает, так это<F><any>function() { ... }
galenus
когда вы используете tslint: рекомендуется, приведение типов не работает, и вам нужно использовать следующее: (заключите функцию в скобки, чтобы сделать ее выражением; затем используйте любое значение, как F):var f = (function () {}) as any as F;
NikhilWanpal
81

Теперь это легко достижимо (машинописный текст 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
Ошибка: число не может быть присвоено строке [] (тип возврата)

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

Мейрион Хьюз
источник
1
Отлично, спасибо. По состоянию на февраль 2017 года это лучший ответ (для пользователей Typescript 2.x).
Эндрю Фолкнер
47

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. Это один из немногих случаев, когда было бы целесообразно смешивать внутренние модули (пространства имен) с внешними.

мк.
источник
Проголосуйте за упоминание внешних модулей в ответе. Это очень типичный случай при преобразовании или аннотировании существующих модулей JS. Именно то, что я ищу!
Nipheris
Прекрасно. Если вы хотите использовать это с модулями ES6, вы можете просто сделать function hello() { .. } namespace hello { export const value = 5; } export default hello;IMO, это намного чище, чем Object.assign или аналогичные хаки времени выполнения. Ни времени выполнения, ни утверждений типа, ничего.
skrebbel
Это замечательный ответ, но как присоединить к функции символ, такой как Symbol.iterator?
kzh
Ссылка выше больше не работает, правильным будет typescriptlang.org/docs/handbook/declaration-merging.html
iJungleBoy
Вы должны знать, что объединение деклараций приводит к довольно нестабильному JavaScript. Он добавляет дополнительное закрытие, которое мне кажется чрезмерным для простого присвоения свойств. Это очень не-JavaScript способ сделать это. Он также вводит проблему порядка зависимостей, когда результирующее значение не обязательно является функцией, если вы не уверены, что функция - это первое, что требуется .
Джон Лейдегрен
2

В качестве ярлыка вы можете динамически назначать значение объекта с помощью метода доступа ['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

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

Рик Лав
источник
1

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

Вот общий помощник, который считывает свойства некоторого объекта 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 . Обратите внимание, что мы ограничились 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;
}
Дэниел Эрвикер
источник
1

Старый вопрос, но для версий 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'"

Ссылка и онлайн-пример .

Geo1088
источник
0

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

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

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

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

WillSeitz
источник