Skip to content

Commit 0d8ee6a

Browse files
authored
Provide property types for public and protected properties (#1126)
1 parent adb5f16 commit 0d8ee6a

File tree

11 files changed

+364
-10
lines changed

11 files changed

+364
-10
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import type { Employee } from './employee';
2+
import type { Name } from './entity';
3+
import type { EventProvider } from './eventProvider';
4+
import { LegalEntity, type RegistrationNumber } from './legalEntity';
5+
import type { Age } from './person';
6+
7+
export class Company extends LegalEntity {
8+
9+
private employees: Employee[] = [];
10+
11+
constructor(companyName: Name, registrationNumber: RegistrationNumber) {
12+
super(companyName, registrationNumber);
13+
}
14+
15+
public listen(eventProvider: EventProvider): void {
16+
eventProvider.onEmployeeAdded((employee: Employee) => {
17+
this.addEmployee(employee);
18+
19+
});
20+
}
21+
22+
public addEmployee(employee: Employee): void {
23+
this.employees.push(employee);
24+
25+
}
26+
27+
public getEmployee(name: string): Employee | undefined {
28+
return this.employees.find(emp => emp.getName() === name);
29+
}
30+
31+
public getAllEmployees(): Employee[] {
32+
return this.employees;
33+
}
34+
35+
public averageEmployeeAge(): number {
36+
let totalAge = 0;
37+
for (const employee of this.employees) {
38+
const age: Age = employee.getAge();
39+
totalAge += age.value;
40+
}
41+
return this.employees.length === 0 ? 0 : totalAge / this.employees.length;
42+
}
43+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
export interface Disposable {
7+
/**
8+
* Dispose this object.
9+
*/
10+
dispose(): void;
11+
}
12+
13+
export namespace Disposable {
14+
export function create(func: () => void): Disposable {
15+
return {
16+
dispose: func
17+
};
18+
}
19+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Person, type Age } from './person';
2+
3+
export class Employee extends Person {
4+
private employeeId: number;
5+
6+
constructor(name: string, age: Age, employeeId: number) {
7+
super(name, age);
8+
this.employeeId = employeeId;
9+
}
10+
11+
getEmployeeDetails(): string {
12+
return `ID: ${this.employeeId}, Name: ${this.getName()}, Age: ${this.getAge().value}`;
13+
}
14+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export class Name {
2+
constructor(private _value: string) {
3+
if (_value.trim().length === 0) {
4+
throw new Error('Name cannot be empty.');
5+
}
6+
}
7+
public get value(): string {
8+
return this._value;
9+
}
10+
}
11+
12+
export class Entity {
13+
14+
constructor(protected name: Name) {
15+
}
16+
17+
getName(): Name {
18+
return this.name;
19+
}
20+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import type { Employee } from './employee';
2+
import type { Event } from './events';
3+
4+
export interface EventProvider {
5+
onEmployeeAdded: Event<Employee>;
6+
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/* --------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
* ------------------------------------------------------------------------------------------ */
5+
6+
import { Disposable } from './disposable';
7+
8+
/**
9+
* Represents a typed event.
10+
*/
11+
export interface Event<T> {
12+
13+
/**
14+
*
15+
* @param listener The listener function will be called when the event happens.
16+
* @param thisArgs The 'this' which will be used when calling the event listener.
17+
* @param disposables An array to which a {{IDisposable}} will be added.
18+
* @return
19+
*/
20+
(listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]): Disposable;
21+
}
22+
23+
export namespace Event {
24+
const _disposable = { dispose() { } };
25+
export const None: Event<any> = function () { return _disposable; };
26+
}
27+
28+
class CallbackList {
29+
30+
private _callbacks: Function[] | undefined;
31+
private _contexts: any[] | undefined;
32+
33+
public add(callback: Function, context: any = null, bucket?: Disposable[]): void {
34+
if (!this._callbacks) {
35+
this._callbacks = [];
36+
this._contexts = [];
37+
}
38+
this._callbacks.push(callback);
39+
this._contexts!.push(context);
40+
41+
if (Array.isArray(bucket)) {
42+
bucket.push({ dispose: () => this.remove(callback, context) });
43+
}
44+
}
45+
46+
public remove(callback: Function, context: any = null): void {
47+
if (!this._callbacks) {
48+
return;
49+
}
50+
51+
let foundCallbackWithDifferentContext = false;
52+
for (let i = 0, len = this._callbacks.length; i < len; i++) {
53+
if (this._callbacks[i] === callback) {
54+
if (this._contexts![i] === context) {
55+
// callback & context match => remove it
56+
this._callbacks.splice(i, 1);
57+
this._contexts!.splice(i, 1);
58+
return;
59+
} else {
60+
foundCallbackWithDifferentContext = true;
61+
}
62+
}
63+
}
64+
65+
if (foundCallbackWithDifferentContext) {
66+
throw new Error('When adding a listener with a context, you should remove it with the same context');
67+
}
68+
}
69+
70+
public invoke(...args: any[]): any[] {
71+
if (!this._callbacks) {
72+
return [];
73+
}
74+
75+
const ret: any[] = [],
76+
callbacks = this._callbacks.slice(0),
77+
contexts = this._contexts!.slice(0);
78+
79+
for (let i = 0, len = callbacks.length; i < len; i++) {
80+
try {
81+
ret.push(callbacks[i]!.apply(contexts[i], args));
82+
} catch (e) {
83+
// eslint-disable-next-line no-console
84+
console.error(e);
85+
}
86+
}
87+
return ret;
88+
}
89+
90+
public isEmpty(): boolean {
91+
return !this._callbacks || this._callbacks.length === 0;
92+
}
93+
94+
public dispose(): void {
95+
this._callbacks = undefined;
96+
this._contexts = undefined;
97+
}
98+
}
99+
100+
export interface EmitterOptions {
101+
onFirstListenerAdd?: Function;
102+
onLastListenerRemove?: Function;
103+
}
104+
105+
export class Emitter<T> {
106+
107+
private static _noop = function () { };
108+
109+
private _event: Event<T> | undefined;
110+
private _callbacks: CallbackList | undefined;
111+
112+
constructor(private _options?: EmitterOptions) {
113+
}
114+
115+
/**
116+
* For the public to allow to subscribe
117+
* to events from this Emitter
118+
*/
119+
get event(): Event<T> {
120+
if (!this._event) {
121+
this._event = (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => {
122+
if (!this._callbacks) {
123+
this._callbacks = new CallbackList();
124+
}
125+
if (this._options && this._options.onFirstListenerAdd && this._callbacks.isEmpty()) {
126+
this._options.onFirstListenerAdd(this);
127+
}
128+
this._callbacks.add(listener, thisArgs);
129+
130+
const result: Disposable = {
131+
dispose: () => {
132+
if (!this._callbacks) {
133+
// disposable is disposed after emitter is disposed.
134+
return;
135+
}
136+
137+
this._callbacks.remove(listener, thisArgs);
138+
result.dispose = Emitter._noop;
139+
if (this._options && this._options.onLastListenerRemove && this._callbacks.isEmpty()) {
140+
this._options.onLastListenerRemove(this);
141+
}
142+
}
143+
};
144+
if (Array.isArray(disposables)) {
145+
disposables.push(result);
146+
}
147+
148+
return result;
149+
};
150+
}
151+
return this._event;
152+
}
153+
154+
/**
155+
* To be kept private to fire an event to
156+
* subscribers
157+
*/
158+
fire(event: T): any {
159+
if (this._callbacks) {
160+
this._callbacks.invoke.call(this._callbacks, event);
161+
}
162+
}
163+
164+
dispose() {
165+
if (this._callbacks) {
166+
this._callbacks.dispose();
167+
this._callbacks = undefined;
168+
}
169+
}
170+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Entity, type Name } from './entity';
2+
3+
export class RegistrationNumber {
4+
constructor(private _value: string) {
5+
if (!/^[A-Z0-9]{8,12}$/.test(_value)) {
6+
throw new Error('Registration number must be 8-12 alphanumeric characters.');
7+
}
8+
}
9+
public get value(): string {
10+
return this._value;
11+
}
12+
}
13+
14+
export class LegalEntity extends Entity {
15+
constructor(name: Name, private registrationNumber: RegistrationNumber) {
16+
super(name);
17+
}
18+
19+
public getRegistrationNumber(): RegistrationNumber {
20+
return this.registrationNumber;
21+
}
22+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { Age, Person } from "./person.js";
2+
3+
const person = new Person("Dirk", new Age(42));
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
export class Age {
2+
constructor(private _value: number) {
3+
if (_value < 0 || _value > 150) {
4+
throw new Error('Age must be between 0 and 150.');
5+
}
6+
}
7+
public get value(): number {
8+
return this._value;
9+
}
10+
}
11+
12+
export class Person {
13+
constructor(private name: string, private age: Age) {
14+
}
15+
public getName(): string {
16+
return this.name;
17+
}
18+
public getAge(): Age {
19+
return this.age;
20+
}
21+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
// Visit https://aka.ms/tsconfig to read more about this file
3+
"compilerOptions": {
4+
// File Layout
5+
"rootDir": "./src",
6+
"outDir": "./dist",
7+
8+
// Environment Settings
9+
// See also https://aka.ms/tsconfig/module
10+
"module": "nodenext",
11+
"target": "esnext",
12+
"types": [],
13+
// For nodejs:
14+
// "lib": ["esnext"],
15+
// "types": ["node"],
16+
// and npm install -D @types/node
17+
18+
// Other Outputs
19+
"sourceMap": true,
20+
"declaration": true,
21+
"declarationMap": true,
22+
23+
// Stricter Typechecking Options
24+
"noUncheckedIndexedAccess": true,
25+
"exactOptionalPropertyTypes": true,
26+
27+
// Style Options
28+
// "noImplicitReturns": true,
29+
// "noImplicitOverride": true,
30+
// "noUnusedLocals": true,
31+
// "noUnusedParameters": true,
32+
// "noFallthroughCasesInSwitch": true,
33+
// "noPropertyAccessFromIndexSignature": true,
34+
35+
// Recommended Options
36+
"strict": true,
37+
"jsx": "react-jsx",
38+
"verbatimModuleSyntax": false,
39+
"isolatedModules": true,
40+
"noUncheckedSideEffectImports": true,
41+
"moduleDetection": "force",
42+
"skipLibCheck": true,
43+
}
44+
}

0 commit comments

Comments
 (0)