Skip to content

Commit 1562fee

Browse files
author
The Buildbot
committed
Auto-generated commit
Triggered by commit: f9e54f8
1 parent 2237b5f commit 1562fee

File tree

5 files changed

+5
-5
lines changed

5 files changed

+5
-5
lines changed

2020-01-everything-github/entry.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"slug":"2022-11-use-define-for-class-fields","html":"<p>Did you know that properties are implemented slightly differently in JavaScript and TypeScript and that there is an incompatible behaviour?\nThat&#39;s why projects with Angular 15 have the option <code>useDefineForClassFields</code> set in their TypeScript configuration.\nWe will show you in detail what the problem is and how you should write your code so that it is future-proof for both programming languages.</p>\n<h2 id=\"contents\">Contents</h2>\n<ul>\n<li><a href=\"/blog/2022-11-use-define-for-class-fields#property-initialisation-in-typescript\">Property initialisation in TypeScript</a></li>\n<li><a href=\"/blog/2022-11-use-define-for-class-fields#the-proprietary-behaviour-of-typescript\">The proprietary behaviour of TypeScript</a></li>\n<li><a href=\"/blog/2022-11-use-define-for-class-fields#future-proof-property-initialisation\">Future-proof property initialisation</a></li>\n<li><a href=\"/blog/2022-11-use-define-for-class-fields#implications-for-existing-angular-code\">Implications for existing Angular code</a></li>\n</ul>\n<blockquote>\n<p><strong>🇩🇪 This article is available in German language here: <a href=\"https://angular-buch.com/blog/2022-11-use-define-for-class-fields\">TypeScript: useDefineForClassFields – zukünftige Breaking Changes vermeiden</a></strong></p>\n</blockquote>\n<h2 id=\"property-initialisation-in-typescript\">Property initialisation in TypeScript</h2>\n<p>When working with Angular, we regularly initialise properties in our classes.\nFor example, a class property can be initialised with a value directly when it is declared.\nThere is also a shorthand notation that allows us to declare properties automatically via the constructor. \nWe normally use this short form in Angular to request dependencies through DI (Dependency Injection).</p>\n<pre><code class=\"language-ts\"><span class=\"hljs-class\"><span class=\"hljs-keyword\">class</span> <span class=\"hljs-title\">User</span> </span>{\n // direct initialisation\n age = <span class=\"hljs-number\">25</span>;\n\n <span class=\"hljs-regexp\">//</span> short form\n constructor(private currentYear: number) {}\n}\n</code></pre>\n<h2 id=\"the-proprietary-behaviour-of-typescript\">The proprietary behaviour of TypeScript</h2>\n<p>These two notations shown before are proprietary features of TypeScript and have existed since the earliest versions of the language.\nThe JavaScript programming language (or more correctly, the ECMAScript standard) did not fully support class properties at that time, since the standardisation was still in progress.\nDuring the design of TypeScript&#39;s properties, the TS team assumed that the chosen implementation would accurately match the behaviour of a future version of JavaScript on the basis of their best knowledge and belief.\nUnfortunately, that didn&#39;t quite work out – standardisation in ECMAScript has gone a different way over the years.</p>\n<p>The original class properties of TypeScript are implemented in such a way that initialisation with values is always performed as the first statement in the constructor.\nThe results of the two following notations have so far been absolutely identical:</p>\n<pre><code class=\"language-ts\"><span class=\"hljs-class\"><span class=\"hljs-keyword\">class</span> <span class=\"hljs-title\">User</span> </span>{\n age = <span class=\"hljs-number\">25</span>;\n}\n\n// is exactly the same in TypeScript as:\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">class</span> <span class=\"hljs-title\">User</span> </span>{\n age: number;\n\n constructor() {\n this.age = <span class=\"hljs-number\">25</span>;\n }\n}\n</code></pre>\n<p>Unfortunately, in JavaScript, native class properties behave a little differently:\nIt is possible to initialise properties first and execute the constructor <em>afterwards</em>.\nThese are two independent steps in JavaScript.\nIn contrast, in the proprietary implementation of TypeScript, the initialisation of the properties always occurs together with the constructor call.</p>\n<p>This discrepancy between TypeScript and JavaScript is very inconvenient, since TypeScript is supposed to be a superset and should remain compatible with JavaScript as far as possible.\nTo align the two programming languages again, the TypeScript team has introduced a new switch called <code>useDefineForClassFields</code>.\nAs soon as the target of TypeScript is set to <code>ES2022</code>, the default value for this option is <code>true</code>.\nThis means that the native implementation of JavaScript will be used and that the properties will behave in a different way than before.\nDepending on the setting, the following code has two different outputs:</p>\n<pre><code class=\"language-ts\"><span class=\"hljs-keyword\">class</span> <span class=\"hljs-title class_\">User</span> {\n age = <span class=\"hljs-variable language_\">this</span>.<span class=\"hljs-property\">currentYear</span> - <span class=\"hljs-number\">1998</span>;\n\n <span class=\"hljs-title function_\">constructor</span>(<span class=\"hljs-params\"><span class=\"hljs-keyword\">private</span> <span class=\"hljs-attr\">currentYear</span>: <span class=\"hljs-built_in\">number</span></span>) {\n <span class=\"hljs-comment\">// useDefineForClassFields: false --&gt; Current age: 25</span>\n <span class=\"hljs-comment\">// useDefineForClassFields: true --&gt; Current age: NaN</span>\n <span class=\"hljs-variable language_\">console</span>.<span class=\"hljs-title function_\">log</span>(<span class=\"hljs-string\">&#x27;Current age:&#x27;</span>, <span class=\"hljs-variable language_\">this</span>.<span class=\"hljs-property\">age</span>);\n }\n}\n\n<span class=\"hljs-keyword\">const</span> user = <span class=\"hljs-keyword\">new</span> <span class=\"hljs-title class_\">User</span>(<span class=\"hljs-number\">2023</span>);\n</code></pre>\n<p>By using the old proprietary behaviour of TypeScript (<code>useDefineForClassFields: false</code>), an age of <code>25</code> is calculated when the constructor of the class is called with the value <code>2023</code>.\nThe shown code has the following step-by-step flow:</p>\n<ol>\n<li>The constructor is called with the current year.</li>\n<li>The value for the current year is assigned to the property <code>currentYear</code>.</li>\n<li>The property <code>age</code> is initialised, and all values are available for calculation. </li>\n<li>The following message is displayed in the console: <code>Current age: 25</code>.</li>\n</ol>\n<p>However, if we set the option <code>useDefineForClassFields</code> to <code>true</code> in the <code>tsconfig.json</code> file, we get <code>NaN</code> as a result, which stands for <code>Not a Number</code>.\nThe code now runs in a different order:</p>\n<ol>\n<li>The property <code>age</code> is initialised first, but not all values are available for calculation: At this point, the property <code>currentYear</code> is still <code>undefined</code>, so that the subtraction cannot produce a valid result.</li>\n<li>The constructor is then called with the current year. </li>\n<li>The value is assigned to the property <code>currentYear</code>. </li>\n<li>The following message is displayed in the console: <code>Current age: NaN</code>.</li>\n</ol>\n<p>You are invited to explore the different behaviour by yourself in this Stackblitz example:<br><strong><a href=\"https://stackblitz.com/edit/angular-buch-usedefineforclassfields?file=src%2Fapp%2Fapp.component.ts,tsconfig.json\">👉 Demo on Stackblitz: useDefineForClassFields</a></strong></p>\n<h2 id=\"future-proof-property-initialisation\">Future-proof property initialisation</h2>\n<p>We want to improve the previously described source code so that it works independently of the current setting.\nIn order to achieve this, we can explicitly initialise the property as the first command in the constructor:</p>\n<pre><code class=\"language-ts\"><span class=\"hljs-keyword\">class</span> <span class=\"hljs-title class_\">User</span> {\n <span class=\"hljs-attr\">age</span>: <span class=\"hljs-built_in\">number</span>;\n\n <span class=\"hljs-title function_\">constructor</span>(<span class=\"hljs-params\"><span class=\"hljs-keyword\">private</span> <span class=\"hljs-attr\">currentYear</span>: <span class=\"hljs-built_in\">number</span></span>) {\n <span class=\"hljs-variable language_\">this</span>.<span class=\"hljs-property\">age</span> = <span class=\"hljs-variable language_\">this</span>.<span class=\"hljs-property\">currentYear</span> - <span class=\"hljs-number\">1998</span>;\n <span class=\"hljs-variable language_\">console</span>.<span class=\"hljs-title function_\">log</span>(<span class=\"hljs-string\">&#x27;Current age:&#x27;</span>, <span class=\"hljs-variable language_\">this</span>.<span class=\"hljs-property\">age</span>);\n }\n}\n\n<span class=\"hljs-keyword\">const</span> user = <span class=\"hljs-keyword\">new</span> <span class=\"hljs-title class_\">User</span>(<span class=\"hljs-number\">2023</span>);\n</code></pre>\n<p>With this notation, it doesn&#39;t matter whether the proprietary behaviour of TypeScript or the standardised behaviour of JavaScript is active.\nBoth implementations produce the same result.</p>\n<p>Of course, in a real project, we rarely use properties to perform arithmetic.\nHowever, we have to be very careful when using Dependency Injection in Angular, especially when we want to use a service within the property initialisation.\nIn the future, the following notation has the potential to break with an error:</p>\n<pre><code class=\"language-ts\"><span class=\"hljs-comment\">// ⚠️ ATTENTION: This code is not future-proof! ⚠️</span>\n\n<span class=\"hljs-meta\">@Component({ /* ... */ })</span>\nexport <span class=\"hljs-keyword\">class</span> <span class=\"hljs-title class_\">MyComponent</span> {\n <span class=\"hljs-comment\">// this.myService could be undefined!</span>\n <span class=\"hljs-keyword\">data</span> = <span class=\"hljs-keyword\">this</span>.myService.getData();\n\n <span class=\"hljs-keyword\">constructor</span>(<span class=\"hljs-keyword\">private</span> myService: MyDataService) { }\n}\n</code></pre>\n<p>To work around the problem, we should always do the initialisation in the constructor.\nThis way our code is future-proof:</p>\n<pre><code class=\"language-ts\"><span class=\"hljs-meta\">@Component({ /* ... */ })</span>\nexport <span class=\"hljs-keyword\">class</span> <span class=\"hljs-title class_\">MyComponent</span> {\n <span class=\"hljs-keyword\">data</span>: Data;\n\n <span class=\"hljs-keyword\">constructor</span>(<span class=\"hljs-keyword\">private</span> myService: MyDataService) {\n <span class=\"hljs-keyword\">this</span>.<span class=\"hljs-keyword\">data</span> = <span class=\"hljs-keyword\">this</span>.myService.getData();\n }\n}\n</code></pre>\n<p>Another option is to not request the dependency via the constructor at all, but to use the <code>inject()</code> function, instead. This function also offers Dependency Injection but without the constructor.\nEven further, if we need the service instance more than once, we can store the requested dependency in a property and use it from anywhere in the class, as shown below:</p>\n<pre><code class=\"language-ts\">import { inject } from <span class=\"hljs-string\">&#x27;@angular/core&#x27;</span>;\n\n<span class=\"hljs-variable\">@Component</span>({ <span class=\"hljs-regexp\">/* ... */</span> })\nexport <span class=\"hljs-class\"><span class=\"hljs-keyword\">class</span> <span class=\"hljs-title\">MyComponent</span> </span>{\n data = inject(MyDataService).getData();\n otherService = inject(MyOtherService);\n}\n</code></pre>\n<blockquote>\n<p><strong>Hint:</strong> If we want to access injected dependencies at direct property initialisation, we should …</p>\n<ul>\n<li>perform the initialisation in the constructor or</li>\n<li>use the <code>inject()</code> function.</li>\n</ul>\n</blockquote>\n<h2 id=\"implications-for-existing-angular-code\">Implications for existing Angular code</h2>\n<p>As we have seen, the setting of <code>useDefineForClassFields</code> has a huge impact.\nIf the switch would have been left in the default setting for existing Angular projects, there would be a lot of unexpected bugs in all projects.\nTherefore, the Angular team has explicitly disabled the setting for both existing and new projects with Angular 15.\nIn the <code>tsconfig.json</code> file we can find the following settings for that:</p>\n<pre><code class=\"language-json\">{\n <span class=\"hljs-string\">&quot;compilerOptions&quot;</span>: {\n <span class=\"hljs-string\">//</span> <span class=\"hljs-string\">...</span>\n <span class=\"hljs-string\">&quot;useDefineForClassFields&quot;</span>: <span class=\"hljs-literal\">false</span>,\n <span class=\"hljs-string\">&quot;target&quot;</span>: <span class=\"hljs-string\">&quot;ES2022&quot;</span>\n }\n}\n</code></pre>\n<p>The well-known proprietary behaviour will therefore remain in place for now.</p>\n<p>However, Angular usually follows the recommendations and defaults of TypeScript.\nFor example, in the past the strict type checks were enabled for new projects.\nWe asssume that one day the setting <code>useDefineForClassFields</code> will be turned to the default value <code>true</code> for new Angular projects.\nWe therefore recommend to already develop your code as solid as possible now and set the <code>useDefineForClassFields</code> setting to <code>true</code> before Angular does it by default.\nIf the default setting will be changed in future, you will not be affected by any breaking change!</p>\n<h2 id=\"workshops-for-your-team\">Workshops for your team</h2>\n<p>The two authors of this article offer Angular training courses in German. \nAll our lessons are always up to date, of course. We only teach examples that are compatible with JavaScript behaviour.\nLearn Angular and best practices together with us and <strong><a href=\"https://angular.schule/#anfrage\">ask now for an offer</a></strong>.</p>\n<hr>\n\n<p><small><strong>Cover image:</strong> Mols Bjerge National Park, Denmark, 2022. Photo by Ferdinand Malcher</small></p>\n","meta":{"title":"TypeScript: useDefineForClassFields – How to avoid future Breaking Changes","author":"Johannes Hoppe and Ferdinand Malcher","mail":"[email protected]","published":"2022-11-29T00:00:00.000Z","lastModified":"2022-11-29T00:00:00.000Z","keywords":["Angular","JavaScript","ECMAScript","TypeScript","ES2022","Class Properties","useDefineForClassFields"],"language":"en","header":{"url":"usedefineforclassfields.jpg","width":2000,"height":881},"sticky":false}}
1+
{"slug":"2022-11-use-define-for-class-fields","error":"duplicated mapping key (6:1)\n\n 3 | mail: johannes.hoppe@haushoppe-i ...\n 4 | author2: Ferdinand Malcher\n 5 | mail2: [email protected]\n 6 | mail: [email protected]\n-----^\n 7 | published: 2022-11-29\n 8 | lastModified: 2022-11-29","meta":{"title":"A minor error occurred while reading: 2022-11-use-define-for-class-fields","hidden":true}}

0 commit comments

Comments
 (0)