Машинопис - Розширення класу помилок


81

Я намагаюся викинути власну помилку з назвою класу "CustomError", надрукованим у консолі замість "Помилка", без успіху:

class CustomError extends Error { 
    constructor(message: string) {
      super(`Lorem "${message}" ipsum dolor.`);
      this.name = 'CustomError';
    }
}
throw new CustomError('foo'); 

Вихідний результат Uncaught Error: Lorem "foo" ipsum dolor .

Що я очікую: Uncaught CustomError: Lorem "foo" ipsum dolor .

Цікаво, чи можна це зробити, використовуючи лише TS (не возившись з прототипами JS)?

Відповіді:


78

Чи використовуєте ви версію машинопису 2.1 та перекладаєте на ES5? Перегляньте цей розділ сторінки останніх змін щодо можливих проблем та способу їх усунення: https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array- and-map-may-no-longer-work

Відповідний біт:

Як рекомендацію, ви можете вручну налаштувати прототип відразу після будь-яких супер (...) дзвінків.

class FooError extends Error {
    constructor(m: string) {
        super(m);

        // Set the prototype explicitly.
        Object.setPrototypeOf(this, FooError.prototype);
    }

    sayHello() {
        return "hello " + this.message;
    }
}

Однак будь-якому підкласу FooError також доведеться вручну встановити прототип. Для середовищ виконання, які не підтримують Object.setPrototypeOf, ви можете замість цього використовувати __proto__.

На жаль, ці обхідні шляхи не працюватимуть в Internet Explorer 10 і раніше. Можна вручну скопіювати методи з прототипу на сам екземпляр (тобто FooError.prototype на цей), але сам ланцюжок прототипів не може бути виправлений.


2
Так, я перекладаю на ES5. Я намагався встановити прототип, як ви запропонували, але, на жаль, це не спрацювало.
darksoulsong

3
Варто зазначити, що, на жаль, тут Object.setPrototypeOf(this, this.constructor.prototype)це не буде працювати, на клас потрібно посилатись явно.
Джон Вайс

1
@JohnWeisz, якщо виправлення, про яке ви згадали, спрацювало, то спочатку не буде нічого виправляти, якщо б ви могли прочитати прототип з this.constructorцього моменту, тоді ланцюжок прототипів вже була б цілою.
thetrompf

3
Станом на машинопис 2.2 здається , є спосіб зробити це без жорсткого кодування точного типу в конструкторі з допомогою: Object.setPrototypeOf(this, new.target.prototype); typescriptlang.org/docs/handbook/release-notes / ...
Grabofus

65

Проблема полягає в тому, що вбудований клас Javascript Errorрозриває ланцюжок прототипів, перемикаючи об’єкт, що будується (тобто this), на новий, інший об’єкт, коли ви телефонуєте, superі цей новий об’єкт не має очікуваного ланцюжка прототипів, тобто це екземпляр з Errorне з CustomError.

Цю проблему можна елегантно вирішити за допомогою 'new.target', який підтримується з Typescript 2.2, див. Тут: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html

class CustomError extends Error {
  constructor(message?: string) {
    // 'Error' breaks prototype chain here
    super(message); 

    // restore prototype chain   
    const actualProto = new.target.prototype;

    if (Object.setPrototypeOf) { Object.setPrototypeOf(this, actualProto); } 
    else { this.__proto__ = actualProto; } 
  }
}

Використання new.targetмає ту перевагу, що вам не потрібно жорстко кодувати прототип, як деякі інші відповіді, запропоновані тут. Це знову має ту перевагу, що класи, що успадковують від CustomError, автоматично також отримають правильний ланцюжок прототипів.

Якби ви жорстко кодували прототип (наприклад Object.setPrototype(this, CustomError.prototype)), CustomErrorвін би мав робочий ланцюжок прототипів, але будь-які класи, що успадковують від, CustomErrorбули б порушені, наприклад, екземпляри a class VeryCustomError < CustomErrorне були б такими, instanceof VeryCustomErrorяк очікувалося, а лишеinstanceof CustomError .

Дивіться також: https://github.com/Microsoft/TypeScript/issues/13965#issuecomment-278570200


Має this.__proto__бути приватним чи державним?
lonix 02

Ви можете зробити це приватним, я думаю, оскільки ви не намагаєтесь отримати доступ до нього за межами конструктора. Отже, якщо ви використовуєте його лише в TypeScript, який компілюється до ES5, це не повинно бути проблемою, оскільки ця функція TypeScript є лише функцією часу компіляції. Поле завжди буде загальнодоступним у складеному коді. На даний момент фактична пропозиція "поля приватного класу" знаходиться лише на експериментальній стадії. І __proto__властивість також застаріла і лише дещо стандартизована, щоб бути дещо зворотно сумісною. Дивіться MDN для отримання додаткової інформації про обидва.
Трве,

1
Або ви можете просто зробити щось подібне (this as any).__proto__ = actualProto;. Це те, що я зробив. Хакі, але тому мені не потрібно змінювати будь-які декларації класів.
Трве

18

Він працює коректно в ES2015 ( https://jsfiddle.net/x40n2gyr/ ). Швидше за все, проблема полягає в тому, що компілятор TypeScript транслірується до ES5 і Errorне може бути правильно підкласифікований лише за допомогою функцій ES5; його можна правильно підкласифікувати лише за допомогою ES2015 і вище ( classабо, що неясніше, Reflect.construct). Це тому, що коли ви телефонуєте Errorяк функцію (а не через newабо, у ES2015, superабо Reflect.construct), вона ігнорує thisта створює нову Error .

Можливо, вам доведеться жити з недосконалим результатом, поки ви не зможете націлитися на ES2015 або вище ...


2
Це. Це. Це і знову це.
Luca T

Як згадувалося в інших відповідях тут, вже є певна робота, тому краще прийняти додаткову складність та здійснити оновлення ланцюжка прототипів.
JoshuaTree

10

Станом на TypeScript 2.2 це можна зробити через new.target.prototype. https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#example

class CustomError extends Error {
    constructor(message?: string) {
        super(message); // 'Error' breaks prototype chain here
        this.name = 'CustomError';
        Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain
    }
}

1
Це не встановлює властивість .name
retorquere

6

Кілька днів тому я зіткнувся з тією ж проблемою у своєму проекті машинопису. Щоб це працювало, я використовую реалізацію від MDN, використовуючи лише ванільні js. Отже, ваша помилка буде виглядати приблизно так:

function CustomError(message) {
  this.name = 'CustomError';
  this.message = message || 'Default Message';
  this.stack = (new Error()).stack;
}
CustomError.prototype = Object.create(Error.prototype);
CustomError.prototype.constructor = CustomError;

throw new CustomError('foo');

Здається, це не працює в фрагменті коду SO, але працює в консолі chrome і в моєму проекті TypeScript:

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


1
Ідея сподобалась, не вдалося реалізувати її на машинописі
Юліус Ш.

Справді? Це працювало над моїм рідним проектом для реагування на машинопис.
Mμ.

Ну, я також хотів абстрагувати шаблонну частину, і, мабуть, не вдалося це зробити, спробую ще раз без будь-яких інших синанігенів.
Юліус Ш.

Хороша ідея, але він зазнає невдачі з цим повідомленням , коли strict: trueв TSconfig: stackoverflow.com/questions/43623461 / ...
Андре

Це працює, але порушує форматування помилок у консолі браузера. На консолі Chrome ваші екземпляри CustomError відображаються не зі звичайним приємнішим форматуванням, яке отримують "справжні" помилки, а у форматі "загального об'єкта", який важче читати.
Крістіан Ханекамп

2

Я буквально ніколи не публікую публікації на SO, але моя команда працює над проектом TypeScript, і нам потрібно було створити багато власних класів помилок, а також націлити es5. Було б неймовірно нудно робити запропоноване виправлення в кожному окремому класі помилок. Але ми виявили, що змогли мати подальший вплив на всі наступні класи помилок, створивши основний власний клас помилок та маючи решту наших помилок extendцього класу. Всередині цього основного класу помилок ми зробили наступне, щоб отримати нижчий ефект оновлення прототипу:

class MainErrorClass extends Error {
  constructor() {
    super()
    Object.setPrototypeOf(this, new.target.prototype)
  }
}

class SomeNewError extends MainErrorClass {} 

...

Використання new.target.prototypeбуло ключем до того, щоб оновити всі класи помилок успадкування без необхідності оновлювати конструктор кожного з них.

Тільки сподіваючись, це позбавить когось іншого головний біль у майбутньому!


1

Спробуйте це...

class CustomError extends Error { 

  constructor(message: string) {
    super(`Lorem "${message}" ipsum dolor.`)
  }

  get name() { return this.constructor.name }

}

throw new CustomError('foo')

Але це, здається, не використовує вже існуючу функціональність і вимагає від нас повторного впровадження колеса, так би мовити. Або я помиляюся?
JoshuaTree

@JoshuaTree Яку вже існуючу функціональність рішення не використовує? Яке колесо реалізується заново? Ви можете не помилитися, але ваші аргументи можуть бути дещо конкретнішими.
flcoder

Вибачте, моя помилка. Я перевірив машинопис src і бачу, що це інтерфейс, а не фактичний об'єкт із геттерами та сеттерами. Ваша реалізація не замінює жодну існуючу функціональність.
JoshuaTree
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.