diff --git a/docs/5-development/07-ts.md b/docs/5-development/07-ts.md index 295ed5c8e2df..c597714bf6d4 100644 --- a/docs/5-development/07-ts.md +++ b/docs/5-development/07-ts.md @@ -38,14 +38,14 @@ Example: ```ts @customElement("ui5-menu") @event("item-click", { - detail: { - item: { - type: Object, - }, - text: { - type: String, - }, - }, + detail: { + item: { + type: Object, + }, + text: { + type: String, + }, + }, }) class MyClass extends UI5Element { @@ -66,8 +66,8 @@ Example: ```ts class MyClass extends UI5Element { - @property({ type: Boolean }) - open!:boolean; + @property({ type: Boolean }) + open!:boolean; } ``` @@ -170,8 +170,8 @@ always have a default value of `false`. Wrong: ```ts class Button extends UI5Element { - @property({ type: ButtonDesign }) - design: ButtonDesign = ButtonDesign.Default; + @property({ type: ButtonDesign }) + design: ButtonDesign = ButtonDesign.Default; } ``` @@ -179,13 +179,13 @@ Also Wrong: ```ts class Button extends UI5Element { - @property({ type: ButtonDesign }) - design: ButtonDesign; + @property({ type: ButtonDesign }) + design: ButtonDesign; - constructor() { - super(); - this.design = ButtonDesign.Default; - } + constructor() { + super(); + this.design = ButtonDesign.Default; + } } ``` @@ -193,8 +193,8 @@ Correct: ```ts class Button extends UI5Element { - @property({type: ButtonDesign, defaultValue: ButtonDesign.Default }) - design!: ButtonDesign; + @property({type: ButtonDesign, defaultValue: ButtonDesign.Default }) + design!: ButtonDesign; } ``` @@ -213,8 +213,8 @@ Before: * @type {HTMLElement[]} */ "default": { - type: HTMLElement, - propertyName: "items", + type: HTMLElement, + propertyName: "items", } ``` @@ -239,8 +239,8 @@ Before: * @type {HTMLElement[]} */ content: { - type: HTMLElement, - invalidateOnChildChange: true, + type: HTMLElement, + invalidateOnChildChange: true, } ``` @@ -264,7 +264,7 @@ Before: * @type {HTMLElement[]} */ "default": { - type: HTMLElement, + type: HTMLElement, } ``` @@ -338,7 +338,7 @@ There are a couple of rules to follow when creating and using events ```ts type ListItemClickEventDetail { - item: ListItemBase, + item: ListItemBase, } ``` @@ -358,6 +358,352 @@ Then, the users of your component can import the detail type and pass it to `Cus ```ts onItemClick(e: CustomEvent) { - console.log(e.detail.item); + console.log(e.detail.item); +} +``` + +## Conventions and guidelines + +### Conventions +
+ +**1. Rename `"event"` to `"e"` in the `.ts` files as it collides with the `@event` decorator**. + +Since the event decorator is being imported with the `event` keyword + +Example: +```ts +import event from "@ui5/webcomponents-base/dist/decorators/event.js"; +``` +Using the keyword `"event"` as a paramater for our handlers leads to a collision between the parameter and the `@event` decorator.
+```ts +// Before ( which would lead to a name collision now ) + +_onfocusin(event: FocusEvent) { + const target = event.target as ProductSwitchItem; + this._itemNavigation.setCurrentItem(target); + this._currentIndex = this.items.indexOf(target); +} +``` +In order to avoid this and keep consistency, we made a decision to name the parameters in our handlers `"e"` instead. + +```ts +// After + +_onfocusin(e: FocusEvent) { + const target = e.target as ProductSwitchItem; + + this._itemNavigation.setCurrentItem(target); + this._currentIndex = this.items.indexOf(target); +} +``` +
+ +**2. Initialize all class members directly in the constructor.** + +When creating classes, initialize **all** class members directly in the constructor, and not in another method, called in the constructor. This is to ensure that TypeScript understands that a class member will be always initialized, therefore is not optional.
+ +Example: + +```ts +// Before + +class UI5Element extends HTMLElement { + constructor() { + super(); + this._initializeState(); + } + + _initializeState() { + const ctor = this.constructor; + this._state = { ...ctor.getMetadata().getInitialState() }; + } +} +``` +Before the change, we used to initialize `_state` in the `_initializeState` function. However, after the refactoring to TypeScript, we must do it directly in the constructor, otherwise it is not recognized as **always** initialized. + +```ts +// After + +class UI5Element extends HTMLElement { + _state: State, + + constructor() { + super(); + const ctor = this.constructor as typeof UI5Element; + this._state = { ...ctor.getMetadata().getInitialState() }; + } +} +``` +
+ +**3. Create types for the Event Details.** + +To enhance the quality and readability of our code, we should establish specific types for the `Event Details`. This approach will clearly define the required **`data`** for an event and optimize its usage. Without well-defined `EventDetail` types, we may also encounter naming conflicts between similar event names in various components, leading to potential errors. Implementing `EventDetail` types will effectively resolve this issue. + +- ***3.1 How should we structure the name of our EventDetail type ?*** + +- - In order to be consistent within our project, the latest convention about how we name our EventDetail types is by using the following pattern:
+ +```ts +// File: DayPicker.ts + +// The pattern is +// <> + +type DayPickerChangeEventDetail = { + dates: Array, + timestamp?: number, +} + +class DayPicker extends CalendarPart implements ICalendarPicker { + ... + _selectDate(e: Event, isShift: boolean) { + ... + this.fireEvent("change", { + timestamp: this.timestamp, + dates: this.selectedDates, + }); + } } + +``` +
+ +**4. Use the syntax of `Array` instead of `T[]`.** + +While both notations work the same way, we have chosen to utilize the `Array` notation, as opposed to `T[]`, to maintain consistency with the notations for `Map<>` and `Record<>`. + +For example: + +```ts +// Instead of +let openedRegistry: RegisteredPopUpT[] = []; + +// We’ll use +let openedRegistry: Array = []; ``` +
+ +**5. Use enums over object literals.** + +Instead of using object literals, we have opted for `enums` to enhance **type safety and maintainability**. The use of enums provides compile-time type safety, reducing the potential for errors and making the code easier to manage. It is also important to note that all types in our "types" folder are already represented as `enums`. + +Example: + +```ts +// File: ColorConvension.ts + +// Instead of + +const CSSColors = { + aliceblue: "f0f8ff", + antiquewhite: "faebd7", + aqua: "00ffff", + aquamarine: "7fffd4", +} + +// We’ll use + +enum CSSColors { + aliceblue = "f0f8ff", + antiquewhite = "faebd7", + aqua = "00ffff", + aquamarine = "7fffd4", +} + +``` +
+ +**6. Use the `"keyof typeof"` syntax when accessing dynamically objects with known keys.** + +When accessing dynamically objects with **known** keys, always use the `"keyof typeof"` syntax for improved accuracy. + +Example: + +```ts +// File: ColorConvension.ts + +enum CSSColors { + aliceblue = "f0f8ff", + antiquewhite = "faebd7", + aqua = "00ffff", + aquamarine = "7fffd4", +} +… + +const getRGBColor = (color: string): ColorRGB => { + ... + if (color in CSSColors) { + color = CSSColors[color as keyof typeof CSSColors]; + } + + return HEXToRGB(color); +}; + +``` +# +In the cases where the keys are unknown or uncertain we use the `Record` notation instead of the `{[key]}` notation.
+In short, `Record` is a TypeScript notation for describing an object with keys of `type K` and values of `type T`. + +Example: + +```ts +// File: UI5ElementMetadata.ts +... +type Metadata = { + tag: string, + managedSlots?: boolean, + properties?: Record, + slots?: Record, + events?: Array, + fastNavigation?: boolean, + themeAware?: boolean, + languageAware?: boolean, +}; +``` +
+ +**7. Do not use "any", unless absolutely necessary.** + +The `"any"` type, while powerful, can be a **dangerous** feature as it instructs the TypeScript compiler to ignore type checking for a specific variable or expression. This can result in errors and make the code more complex to understand and maintain. Our `ESLint` usually takes care of this by enforcing best practices and avoiding its usage. + +
+ +### TypeScript specific guidelines +
+ +**1. When to use "import type" ?** + +The `import` keyword is used to import values from a module, while `import type` is used to import only the type information of a module without its values. This type information can be used in type annotations and declarations. +
+ +For clarity, it is recommended to keep ***type*** and ***non-type*** imports on separate lines and explicitly mark types with the `type` keyword, as in the following example: + +```ts +// This line + +import I18nBundle, { getI18nBundle, I18nText } from "@ui5/webcomponents-base/dist/i18nBundle.js"; +``` +```ts + +// Should be split into + +// Named export (function) used into the the component class +import { getI18nBundle } from "@ui5/webcomponents-base/dist/i18nBundle.js"; + +// Default export used into the the component class. +// I18nBundle is a class constructor, but in the current example it's used +// as a type for a variable to which the class will be assinged. +import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js"; + +// named type export +import type { I18nText } from "@ui5/webcomponents-base/dist/i18nBundle.js"; +``` + +
+ +**2. When should we use the `"!"` operator in component's file ?** + +The `!` operator in TypeScript is used to indicate that a value is not `null` or `undefined` in situations where the type checker cannot determine it. + +It is commonly used when working with the `this.getDomRef()` and `this.shadowRoot` properties in our web components. The return types of these properties, `HTMLElement | null` and `ShadowRoot | null`, respectively, are marked with `null` because there may be instances when these values are not yet available. + +This operator can also be used in other situations where TypeScript does not understand the framework's lifecycle, for example, when working with custom elements. + +In short, the `!` operator is a useful tool for ensuring that a value is not `null` or `undefined` in cases where the type checker cannot determine this on its own. + +For example: + +```ts +import UI5Element from "sap/ui/core/Element"; + +class Example extends UI5Element { + testProperty?: string; + + onBeforeRendering() { + this.testProperty = "Some text"; + } + + onAfterRendering() { + // here TypeScript will complain about that the testProperty may be undefined + // in order of its definition and because it doesn't understand the framework's lifecycle + const varName: string = this.testProperty!; + } +} +``` +
+ +**3. Usage of Generics.** + +Generics in TypeScript help us with the creation of classes, functions, and other entities that can work with multiple types, instead of just a single one. This allows users to use their own types when consuming these entities. + +In the migration to TypeScript, generic functions have been added to the `UI5Element`, and a common approach for using built-in generics has been established. + +Our first generic function is the `fireEvent` function, which uses generics to describe the event details and to check that all necessary details have been provided. The types used to describe the details provide helpful information to consumers of the event as explained above. + +For example: + +```ts +fireEvent("click") +``` +
+ +The use of custom events as the type for the first argument of an event handler can result in TypeScript complaining about unknown properties in the details. By using generics and introducing a type for event details, we can tell Typescript which parameters are included in the details, and thus avoid these complaints. + +```ts +handleClick(e: CustomEvent) +``` + +The second use of generics is in the `querySelector` function. It allows us to specify a custom element return type, such as "List," while retaining the default return type of `T | null.` This allows for more precise type checking and better understanding of the expected return value. + +It's important to note that casting the returned result will exclude "`null`." Additionally, if the result is always in the template and not surrounded by expressions, the "!" operator can be used. + + + +```ts +async _getDialog() { + const staticAreaItem = await this.getStaticAreaItemDomRef(); + return staticAreaItem!.querySelector("[ui5-dialog]")!; +} +``` + +The third use case for generics is with the `getFeature` function. This function enables us to retrieve a feature, if it is **registered**. It is important to note that `getFeature` returns the class definition, rather than an instance of the class. To use it effectively, the `typeof` keyword should be utilized to obtain the class type, which will then be set as the return type of the function. + +```ts + getFeature("FormSupport") +``` +
+ +**4. Managing Component Styles with `CSSMap` and `ComponentStylesData` in the Inheritance Chain** + +To resolve inheritance chain issues, we introduced two types that can be used in the components. All components have implemented a static `get styles` function that returns either an array with required styles or just the component styles without an array. However, depending on the inheritance chain, TypeScript may complain about wrong return types, without considering that they will be merged into a flat array in the end. + +```ts +// File: ListItem.ts + +static get styles(): ComponentStylesData { + return [ListItemBase.styles, styles]; +} +``` +
+ +**5. Resolving the `this` type error with TypeScript.** + +By default in Strict Mode, the type of `this` is explicitly `any`. When used in a global context function, as in the example, TypeScript will raise an error that `this` has an explicit type of `any`. To resolve this, you can add `this` as the first argument to the function and provide its type, usually the context in which the function will be used. + +```ts +type MyType = { + base: number; + pow: (exponent: number) => number; +}; + +function pow(this: MyType, exponent: number) { + return Math.pow(this.base, exponent); +} + +const basePow: MyType = { + base: 2, + pow, +}; +``` \ No newline at end of file