TypeScript is a free and open source programming language developed by Microsoft. It is a superset of JavaScript, and essentially adds optional static typing and class-based object-oriented programming to the language.
TypeScript offers the latest and evolving JavaScript features, including those from ECMAScript 2015 and future proposals, such as asynchronous functionality and Decorators, to help build robust components.
How do I explicitly set properties on a Window object
Used for JavaScript developers, for window. MyNamespace = window. MyNamespace | | {}; This line of code is not unfamiliar. To avoid conflicts during development, we usually have separate namespaces for certain functions.
. However, in the TS for window MyNamespace = window. MyNamespace | | {}; For this line of code, the TS compiler prompts the following exception:
Property 'MyNamespace' does not exist on type 'Window & typeof globalThis'.(2339)
Copy the code
The MyNamespace attribute does not exist on the Window & Typeof globalThis crossover type. So how to solve this problem? The simplest way to do this is to use type assertions:
(window as any).MyNamespace = {};
Copy the code
While using any can solve this problem, a better way to solve this problem is to extend the Window interface in the lib.dom.d.ts file as follows:
declare interface Window {
MyNamespace: any;
}
window.MyNamespace = window.MyNamespace || {};
Copy the code
Let’s look again at the Window interface declared in the lib.dom.d.ts file:
/** * A window containing a DOM document; the document property * points to the DOM document loaded in that window. */ interface Window extends EventTarget, AnimationFrameProvider, GlobalEventHandlers, WindowEventHandlers, WindowLocalStorage, WindowOrWorkerGlobalScope, Readonly devicePixelRatio: number; readonly document: Document; readonly top: Window; readonly window: Window & typeof globalThis; addEventListener(type: string, listener: EventListenerOrEventListenerObject, options? : boolean | AddEventListenerOptions): void; removeEventListener<K extends keyof WindowEventMap>(type: K, listener: (this: Window, ev: WindowEventMap[K]) => any, options? : boolean | EventListenerOptions): void; [index: number]: Window; }Copy the code
We declared two Window interfaces with the same name above, and there was no conflict. TypeScript automatically merges interfaces, putting members of both sides into a single interface with the same name.
How do I dynamically assign attributes to objects
In JavaScript, we can easily assign attributes to objects dynamically, such as:
let developer = {};
developer.name = "semlinker";
Copy the code
The above code works fine in JavaScript, but in TypeScript, the compiler prompts the following exception:
Property 'name' does not exist on type '{}'.(2339)
Copy the code
The {} type represents an object that contains no members, so it does not contain the name attribute. To solve this problem, we can declare a LooseObject type:
interface LooseObject {
[key: string]: any
}
Copy the code
The LooseObject type can accept a key as a string and a value as an any field. With the LooseObject type in place, we can solve this problem by:
interface LooseObject {
[key: string]: any
}
let developer: LooseObject = {};
developer.name = "semlinker";
Copy the code
The LooseObject type is very loose. In some application scenarios, we want to be able to declare mandatory and optional attributes in addition to supporting dynamic attributes.
For example, for a Developer interface that represents a Developer, we want the name attribute to be mandatory, the age attribute to be optional, and the string attribute to be set dynamically. For this requirement we can do the following:
interface Developer { name: string; age? : number; [key: string]: any } let developer: Developer = { name: "semlinker" }; developer.age = 30; developer.city = "XiaMen";Copy the code
In addition to using index signatures, we can also use TypeScript’s built-in tool type Record to define the Developer interface:
// type Record<K extends string | number | symbol, T> = { [P in K]: T; } interface Developer extends Record<string, any> { name: string; age? : number; } let developer: Developer = { name: "semlinker" }; developer.age = 30; developer.city = "XiaMen";Copy the code
How to make sense of generics<T>
For readers new to TypeScript generics, the
syntax will be unfamiliar for the first time. There’s nothing special about it, just like passing parameters, we pass the type we want to use for a particular function call.
Referring to the picture above, when we call identity
(1), the Number type is like parameter 1, which fills in the type wherever T occurs. The T inside
in the figure is called the type variable, and it is the type placeholder we want to pass to the identity function, and it is assigned to the value argument to replace its type: T acts as the type, not the specific Number type.
Where T stands for Type and is usually used as the first Type variable name when defining generics. But T can actually be replaced by any valid name. In addition to T, here’s what common generic variables stand for:
- K (Key) : indicates the Key type of the object.
- V (Value) : indicates the Value type of an object.
- E (Element) : indicates the Element type.
Instead of defining only one type variable, we can introduce as many type variables as we wish. For example, we introduce a new type variable U to extend our identity function:
function identity <T, U>(value: T, message: U) : T {
console.log(message);
return value;
}
console.log(identity<Number, string>(68, "Semlinker"));
Copy the code
In addition to explicitly setting values for type variables, it is more common to have the compiler automatically select these types to make the code cleaner. We can omit the Angle brackets entirely, as in:
function identity <T, U>(value: T, message: U) : T {
console.log(message);
return value;
}
console.log(identity(68, "Semlinker"));
Copy the code
For the code above, the compiler was smart enough to know our parameter types and assign them to T and U without the developer specifying them explicitly.
How to understand the role of decorators
Decorators in TypeScript fall into four categories: class decorators, property decorators, method decorators, and parameter decorators. A decorator is essentially a function that allows you to easily define object-related metadata.
For example, in the Ionic-Native project, it uses the Plugin decorator to define information about the Device plug-in in IonicNative:
@Plugin({
pluginName: 'Device',
plugin: 'cordova-plugin-device',
pluginRef: 'device',
repo: 'https://github.com/apache/cordova-plugin-device',
platforms: ['Android', 'Browser', 'iOS', 'macOS', 'Windows'],
})
@Injectable()
export class Device extends IonicNativePlugin {}
Copy the code
The Plugin function in the above code is called a decorator factory, which returns a class decorator to decorate the Device class. The Plugin factory function is defined as follows:
// https://github.com/ionic-team/ionic-native/blob/v3.x/src/%40ionic-native/core/decorators.ts export function Plugin(config: PluginConfig): ClassDecorator { return function(cls: For (let prop in config) {CLS [prop] = config[prop]; } cls['installed'] = function(printWarning? : boolean) { return !! getPlugin(config.pluginRef); }; Return CLS; }; }Copy the code
By looking at the method signature of the Plugin factory function, we can see that calling the function returns an object of type ClassDecorator, which is declared as follows:
declare type ClassDecorator = <TFunction extends Function>(target: TFunction)
=> TFunction | void;
Copy the code
Class decorators are, as the name suggests, used to decorate classes. It takes an argument, Target: TFunction, which represents the class to be decorated. With that said, let’s look at another issue @plugin ({… }) what is the use of the @ sign?
Actually @ Plugin ({… }) is just syntax sugar. Why is it syntax sugar? Here’s a look at the compiled ES5 code:
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var Device = /** @class */ (function (_super) { __extends(Device, _super); function Device() { return _super ! == null && _super.apply(this, arguments) || this; } Device = __decorate([ Plugin({ pluginName: 'Device', plugin: 'cordova-plugin-device', pluginRef: 'device', repo: 'https://github.com/apache/cordova-plugin-device', platforms: ['Android', 'Browser', 'iOS', 'macOS', 'Windows'], }), Injectable() ], Device); return Device; }(IonicNativePlugin));Copy the code
From the generated code, the @plugin ({… }) and @Injectable() are eventually converted to normal method calls, and the result of those calls is passed as an array argument to __decorate, while the Device class is called as an argument inside __decorate. Thus extending the corresponding functions.
In addition, if you have used Angular, you will be familiar with the following code.
const API_URL = new InjectionToken('apiUrl');
@Injectable()
export class HttpService {
constructor(
private httpClient: HttpClient,
@Inject(API_URL) private apiUrl: string
) {}
}
Copy the code
In the HttpService class decorated with the Injectable class decorator, we inject the HttpClient dependency object used to process HTTP requests by constructing an injection. The object corresponding to the API_URL is injected through the Inject parameter decorator, which is called Dependency Injection.
How to understand function overloading
Cute and hateful union type
Since JavaScript is a dynamic language, we usually call the same function with different types of arguments, and the function returns different types of results depending on the arguments:
function add(x, y) { return x + y; } add(1, 2); // 3 add("1", "2"); / / "12"Copy the code
Since TypeScript is a superset of JavaScript, the above code can be used directly in TypeScript. However, when the TypeScript compiler turns on the noImplicitAny configuration item, this code prompts the following error message:
Parameter 'x' implicitly has an 'any' type.
Parameter 'y' implicitly has an 'any' type.
Copy the code
This information tells us that arguments x and y implicitly have type any. To solve this problem, we can set a type for the parameter. Because we want to add the function at the same time support the string and number types, so we can define a string | number joint type, at the same time we take individual name for the joint type:
type Combinable = string | number;
Copy the code
After defining the Combinable union type, let’s update the add function:
function add(a: Combinable, b: Combinable) {
if (typeof a === 'string' || typeof b === 'string') {
return a.toString() + b.toString();
}
return a + b;
}
Copy the code
When you explicitly type the parameters of the add function, the error message disappears. Now that the add function is perfect, let’s actually test it:
const result = add('semlinker', ' kakuqo');
result.split(' ');
Copy the code
In the above code, we call add with the strings ‘semlinker’ and ‘Kakuqo’ and save the result to a variable named result, which we assume is of type string. So we can call the split method on the string object normally. The TypeScript compiler now displays the following error message:
Property 'split' does not exist on type 'Combinable'.
Property 'split' does not exist on type 'number'.
Copy the code
It is clear that split properties do not exist on Combinable and number objects. Here comes the problem. How to solve it? At this point we can take advantage of TypeScript’s function overloading.
Function overloading
Function overloading or method overloading is the ability to create multiple methods with the same name and different numbers or types of arguments.
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: string, b: number): string;
function add(a: number, b: string): string;
function add(a: Combinable, b: Combinable) {
// type Combinable = string | number;
if (typeof a === 'string' || typeof b === 'string') {
return a.toString() + b.toString();
}
return a + b;
}
Copy the code
In the above code, we provide multiple function type definitions for the Add function to enable function overloading. In TypeScript, we can override member methods in a class in addition to normal functions.
Method overloading refers to a technique in which a method with the same name and different parameters (different parameter types, different number of parameters, or different sequence of parameters with the same number of parameters) is called based on the form of arguments and the method matching the operation is selected. So member methods in a class can be overloaded only if they have the same method name and different argument lists in the same class. Here’s an example of an overloaded member method:
class Calculator {
add(a: number, b: number): number;
add(a: string, b: string): string;
add(a: string, b: number): string;
add(a: number, b: string): string;
add(a: Combinable, b: Combinable) {
if (typeof a === 'string' || typeof b === 'string') {
return a.toString() + b.toString();
}
return a + b;
}
}
const calculator = new Calculator();
const result = calculator.add('Semlinker', ' Kakuqo');
Copy the code
It’s important to note here that when the TypeScript compiler handles function overloads, it looks up the list of overloads and tries to use the first overloaded definition. Use this if it matches. Therefore, when defining overloads, always put the most precise definition first. Also, in the Calculator class, add(a: Combinable, b: Combinable){} is not part of the overload list, so we define only four overloaded methods for the Add member methods.
What is the difference between interfaces and type
- Objects/Functions
Both interface and type aliases can be used to describe object shapes or function signatures:
interface
interface Point {
x: number;
y: number;
}
interface SetPoint {
(x: number, y: number): void;
}
Copy the code
Type the alias
type Point = {
x: number;
y: number;
};
type SetPoint = (x: number, y: number) => void;
Copy the code
- Other Types
Unlike interface types, type aliases can be used for some other types, such as primitive types, union types, and tuples:
// primitive
type Name = string;
// object
type PartialPointX = { x: number; };
type PartialPointY = { y: number; };
// union
type PartialPoint = PartialPointX | PartialPointY;
// tuple
type Data = [number, string];
Copy the code
- Extend
Both interface and type aliases can be extended, but the syntax is different. In addition, interface and type aliases are not mutually exclusive. Interfaces can extend type aliases, but not the other way around.
Interface extends interface
interface PartialPointX { x: number; }
interface Point extends PartialPointX {
y: number;
}
Type alias extends type alias
type PartialPointX = { x: number; };
type Point = PartialPointX & { y: number; };
Interface extends type alias
type PartialPointX = { x: number; };
interface Point extends PartialPointX { y: number; }
Type alias extends interface
interface PartialPointX { x: number; }
type Point = PartialPointX & { y: number; };
Copy the code
- Implements
Classes can implement interface or type aliases in the same way, but classes cannot implement union types defined using type aliases:
interface Point {
x: number;
y: number;
}
class SomePoint implements Point {
x = 1;
y = 2;
}
type Point2 = {
x: number;
y: number;
};
class SomePoint2 implements Point2 {
x = 1;
y = 2;
}
type PartialPoint = { x: number; } | { y: number; };
// A class can only implement an object type or
// intersection of object types with statically known members.
class SomePartialPoint implements PartialPoint { // Error
x = 1;
y = 2;
}
Copy the code
- Declaration merging
Unlike type aliases, interfaces can be defined multiple times and are automatically merged into a single interface.
interface Point { x: number; }
interface Point { y: number; }
const point: Point = { x: 1, y: 2 };
Copy the code
What’s the difference between object, object and {}
object
type
The object type is: a new type introduced in TypeScript 2.2 that represents non-primitive types.
// node_modules/typescript/lib/lib.es5.d.ts
interface ObjectConstructor {
create(o: object | null): any;
// ...
}
const proto = {};
Object.create(proto); // OK
Object.create(null); // OK
Object.create(undefined); // Error
Object.create(1337); // Error
Object.create(true); // Error
Object.create("oops"); // Error
Copy the code
- The Object type
Object type: This is the type of all instances of the Object class and is defined by the following two interfaces:
Object
The interface definesObject.prototype
Properties on the prototype object;
// node_modules/typescript/lib/lib.es5.d.ts
interface Object {
constructor: Function;
toString(): string;
toLocaleString(): string;
valueOf(): Object;
hasOwnProperty(v: PropertyKey): boolean;
isPrototypeOf(v: Object): boolean;
propertyIsEnumerable(v: PropertyKey): boolean;
}
Copy the code
ObjectConstructor
The interface definesObject
Class properties.
// node_modules/typescript/lib/lib.es5.d.ts interface ObjectConstructor { /** Invocation via `new` */ new(value?: any): Object; /** Invocation via function calls */ (value? : any): any; readonly prototype: Object; getPrototypeOf(o: any): any; //...} declare var Object: ObjectConstructor;Copy the code
All instances of the Object class inherit all properties of the Object interface.
{}
type
The {} type describes an object that has no members. TypeScript generates a compile-time error when you try to access arbitrary properties of such an object.
// Type {}
const obj = {};
// Error: Property 'prop' does not exist on type '{}'.
obj.prop = "semlinker";
Copy the code
However, you can still use all the properties and methods defined on the Object type, which can be used implicitly through JavaScript’s prototype chain:
// Type {}
const obj = {};
// "[object Object]"
obj.toString();
Copy the code
What is the difference between numeric enumeration and string enumeration
- Digital enumeration
Boolean variables in JavaScript have a limited range of values, namely true and false. With enumerations in TypeScript, you can customize similar types:
enum NoYes {
No,
Yes,
}
Copy the code
No and Yes are called members of the enumerated NoYes. Each enumerator has a name and a value. The default type of a numeric enumerator value is number. That is, the value of each member is a number:
enum NoYes {
No,
Yes,
}
assert.equal(NoYes.No, 0);
assert.equal(NoYes.Yes, 1);
Copy the code
Instead of having TypeScript specify the values of enumeration members for us, we can assign them manually:
enum NoYes {
No = 0,
Yes = 1,
}
Copy the code
This explicit assignment via the equals sign is called initializer. If the value of a member in the enumeration is explicitly assigned, but no subsequent member is assigned, TypeScript increments the value of the subsequent member based on the current member’s value.
- String enumeration
In addition to numeric enumerations, we can also use strings as enumerator values:
enum NoYes {
No = 'No',
Yes = 'Yes',
}
assert.equal(NoYes.No, 'No');
assert.equal(NoYes.Yes, 'Yes');
Copy the code
- Numeric enumeration vs string enumeration
What is the difference between numeric enumeration and string enumeration? Here we look at the result of compiling numeric enumerations and string enumerations respectively:
Numeric enumeration results of compilation
"use strict";
var NoYes;
(function (NoYes) {
NoYes[NoYes["No"] = 0] = "No";
NoYes[NoYes["Yes"] = 1] = "Yes";
})(NoYes || (NoYes = {}));
Copy the code
String enumeration of compiled results
"use strict";
var NoYes;
(function (NoYes) {
NoYes["No"] = "No";
NoYes["Yes"] = "Yes";
})(NoYes || (NoYes = {}));
Copy the code
Looking at the above results, we know that numeric enumeration supports reverse mapping from member name to member name in addition to the normal mapping from member name to member value. Also, for pure string enumerations, we cannot omit any initializer. Numeric enumerations, if not explicitly set, are initialized with default values.
- Allocates out-of-bounds values for numeric enumerations
Speaking of enumerations, here’s another problem:
const enum Fonum {
a = 1,
b = 2
}
let value: Fonum = 12; // Ok
Copy the code
Let value: Fonum = 12; This line, TS compiler does not prompt any error will be surprised. It is clear that the number 12 is not a member of Fonum’s enumeration. Why is that? Take a look at DanielRosenwasser’s answer to TypeScript issues 26362:
The behavior is motivated by bitwise operations. There are times when SomeFlag.Foo | SomeFlag.Bar is intended to produce another SomeFlag. Instead you end up with number, and you don’t want to have to cast back to SomeFlag.
This behavior is caused by bitwise operations. Sometimes SomeFlag. Foo | SomeFlag. The Bar is used to generate another SomeFlag. Instead, you end up with numbers, and you don’t want to force back to SomeFlag.
Let value: Fonum = 12; This statement, which the TS compiler does not error, is because the number 12 can be computed from Fonum’s existing enumerators.
let value: Fonum = Fonum.a << Fonum.b << Fonum.a | Fonum.a << Fonum.b; / / 12Copy the code
What is the difference between a private field defined with # and a field defined with a private modifier
ECMAScript private fields are supported in TypeScript version 3.8, and can be used as follows:
class Person { #name: string; constructor(name: string) { this.#name = name; } greet() { console.log(`Hello, my name is ${this.#name}! `); } } let semlinker = new Person("Semlinker"); semlinker.#name; // ~~~~~ // Property '#name' is not accessible outside class 'Person' // because it has a private identifier.Copy the code
Unlike regular attributes (even those declared using private modifiers), the following rules should be kept in mind for private fields:
- Private field to
#
The beginning of a character, sometimes called a private name; - Each private field name is uniquely qualified to the class it contains;
- Cannot be used on private fields
TypeScript
Accessibility modifiers (e.gpublic
或private
); - Private fields cannot be accessed outside of the contained class or even detected.
What’s the difference between a private field defined with # and a field defined with a private modifier? Now let’s look at a private example:
class Person {
constructor(private name: string){}
}
let person = new Person("Semlinker");
console.log(person.name);
Copy the code
In the above code, we create a Person class that uses the private modifier to define a private property name. We then use this class to create a Person object and access the Person object’s private properties via Person.name. The TypeScript compiler raises the following exception:
Property 'name' is private and only accessible within class 'Person'.(2341)
Copy the code
So how do you resolve this exception? Of course you can use type assertions to convert person to any:
console.log((person as any).name);
Copy the code
This eliminates TypeScript compiler exceptions, but at runtime we still have access to private properties inside the Person class. Why? Take a look at the compiled ES5 code and you may know the answer:
var Person = /** @class */ (function () { function Person(name) { this.name = name; } return Person; } ()); var person = new Person("Semlinker"); console.log(person.name);Copy the code
Some of you may wonder what TypeScript 3.8 and above generates when it compiles private fields defined by # signs:
class Person {
#name: string;
constructor(name: string) {
this.#name = name;
}
greet() {
console.log(`Hello, my name is ${this.#name}!`);
}
}
Copy the code
The above code is set to ES2015 and will compile and generate the following code:
"use strict"; var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, privateMap, value) { if (! privateMap.has(receiver)) { throw new TypeError("attempted to set private field on non-instance"); } privateMap.set(receiver, value); return value; }; var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, privateMap) { if (! privateMap.has(receiver)) { throw new TypeError("attempted to get private field on non-instance"); } return privateMap.get(receiver); }; var _name; class Person { constructor(name) { _name.set(this, void 0); __classPrivateFieldSet(this, _name, name); } greet() { console.log(`Hello, my name is ${__classPrivateFieldGet(this, _name)}! `); } } _name = new WeakMap();Copy the code
By looking at the code above, ECMAScript private fields defined using # signs are stored via WeakMap objects, Both __classPrivateFieldSet and __classPrivateFieldGet are generated by the compiler to set and retrieve values.