기존에 Java로 작성한 array-based ADT List를 Typescript로 재작성해보았다.
interface List<T> {
readonly size: number;
isEmpty(): boolean;
isFull(): boolean;
add(item: T, index: number): ResultState<T>;
remove(index: number): ResultState<T>;
removeAll(): void;
get(index: number): ResultState<T>;
find(item: T): ResultState<T>;
}
우선 인터페이스에는 operations들과 함께 size라는 데이터 프로퍼티를 정의하였다. 인터페이스 타입의 클래스 객체를 생성한 후 외부에서 접근할 때 변경되지 못하도록 해야하므로 readonly를 추가하였다. 또한 add, remove, get, find 메소드를 호출할 때 우리가 예상할 수 있는 에러에 대해 깔끔하게 처리하기 위해 성공 혹은 실패와 관련된 정보를 담은 ResultState를 반환하게끔 하였다.
type SuccessState<T> = {
state: 'success';
array: Array<T>;
element?: T;
index?: number;
};
type FailureReason =
| 'index out of range'
| 'already full list'
| 'empty list'
| 'invalid value';
type FailureState = {
state: 'fail';
reason: FailureReason;
};
type ResultState<T> = SuccessState<T> | FailureState;
SuccessState와 FailureState는 state 프로퍼티로 구분이 가능한 Discriminated Unions로서 ResultState로 정의하였다. FailureReason은 String literal type들의 Union Type으로 정의하였다.
class ListImpl<T> implements List<T> {
private _size: number = 0;
private readonly MAX_LENGTH = 5;
private array: Array<T> = new Array();
private get RESULT_ARRAY(): SuccessState<T> {
return {
state: 'success',
array: this.array,
};
}
private readonly INDEX_OUT_OF_RANGE: FailureState = {
state: 'fail',
reason: 'index out of range',
};
private readonly FULL_LIST: FailureState = {
state: 'fail',
reason: 'already full list',
};
private readonly EMPTY_LIST: FailureState = {
state: 'fail',
reason: 'empty list',
};
private readonly INVALID_VALUE: FailureState = {
state: 'fail',
reason: 'invalid value',
};
get size() {
return this._size;
}
isEmpty(): boolean {
if (this._size === 0) return true;
else return false;
}
isFull(): boolean {
if (this._size === this.MAX_LENGTH) return true;
return false;
}
add(item: T, index: number): ResultState<T> {
if (this.isFull()) {
return this.FULL_LIST;
}
if (index < 0 || index > this.size) {
return this.INDEX_OUT_OF_RANGE;
}
for (let j = this.size; j > index; j--) {
this.array[j] = this.array[j - 1];
}
this.array[index] = item;
this._size++;
return this.RESULT_ARRAY;
}
remove(index: number): ResultState<T> {
if (this.isEmpty()) {
return this.EMPTY_LIST;
}
if (index < 0 || index >= this.size) {
return this.INDEX_OUT_OF_RANGE;
}
for (let j = index; j < this.size; j++) {
this.array[j] = this.array[j + 1];
}
this._size--;
return this.RESULT_ARRAY;
}
removeAll(): void {
this.array = new Array();
this._size = 0;
}
get(index: number): ResultState<T> {
if (this.isEmpty()) {
return this.EMPTY_LIST;
}
if (index < 0 || index >= this.size) {
return this.INDEX_OUT_OF_RANGE;
}
return {
...this.RESULT_ARRAY,
element: this.array[index],
};
}
find(item: T): ResultState<T> {
if (this.isEmpty()) {
return this.EMPTY_LIST;
}
for (let j = 0; j < this._size; j++) {
if (this.array[j] === item) {
return {
...this.RESULT_ARRAY,
index: j,
};
}
}
return this.INVALID_VALUE;
}
}
인터페이스에서 readonly 프로퍼티로 정의한 size의 경우 클래스 내부에서 똑같이 readonly 프로퍼티로 정의하게 되면 클래스 내부에서 해당 프로퍼티를 변경하지 못하기 때문에, 변경이 가능한 private 프로퍼티인 underscore가 붙은 size 를 선언해주고 이를 반환하는 size의 custom getter를 정의해주었다. 그리고 각각의 메소드 내부에서 성공 혹은 실패(유효하지 않은 인덱스 범위, 꽉 찬 리스트, 빈 리스트, 유효하지 않은 값), 즉 위에서 정의한 각각의 타입을 반환하는 객체 프로퍼티들도 정의해주었다. 이렇게 함으로써 코드의 재사용성을 높여주었다.
const list: List<string> = new ListImpl<string>();
type Fruit = [name: string, index: number];
const fruits: Fruit[] = [['Apple', 0], ['Banana', 1], ['Orange', 2], ['Peach', 1]];
for (let i=0; i<fruits.length; i++) {
const [name, index] = fruits[i];
const result = list.add(name, index);
if (result.state == "success") {
console.log(result.array);
} else {
console.log(result.reason);
}
}
const result2 = list.remove(4);
if (result2.state == "success") {
console.log(result2.array);
} else {
console.log(result2.reason);
}
const result3 = list.get(3);
if (result3.state == "success") {
console.log(result3.element);
} else {
console.log(result3.reason);
}
const result4 = list.find('Peach');
if (result4.state == "success") {
console.log(result4.index);
} else {
console.log(result4.reason);
}
하나의 튜플 타입을 Fruit으로 정의하였고 이러한 튜플의 배열을 담은 객체를 정의하였다. 이를 통해 add, remove, get, find가 각각 반환하는 객체의 state가 "success"냐 혹은 "fail"이냐에 따라서 그 결과를 달리 출력하게끔 해주었다.