Class
typescript
를 쓰는 이유라고 할 수 있는 class
이다.
javascript
에서도 class
문법이 이미 있긴 하지만 우리가 원하는 객체지향 프로그래밍 언어 수준까지는 지원해주지 않기 때문에 javascript
에서의 class
는 그저 좀 더 구체적인 객체처럼 다루기 위해서 쓰는 것에 불과했기 때문이다.
Field
1
2
3
4
5
class Point {
x: number;
y: number;
}
const pt = new Point();
객체의 타입 선언과 비슷한 문법으로 class
의 field
정의를 할 수 있다. (다만 위 코드는 오류가 날 것이다.)
초기화 생략
위 코드를 직접 실행 해봤다면 에러가 표시 되는 것을 확인할 수 있었을 것이다.
class
를 구성하고 있는 변수들은 반드시 constructor
에서 초기화를 진행해줘야 하기 때문이다.
만약 멤버 변수들의 정의만을 하고 싶을 뿐, constructor
에서 초기화를 진행하고 싶지 않다면 확정 할당 연산자(Non-null assertion operator)를 사용한다. !
1
2
3
4
5
6
7
class Point {
x!: number;
y!: number;
}
const pt = new Point();
console.log(pt.x, pt.y); // undefined undefined
멤버 변수 이름 뒤에 !
를 붙이면 constructor
에서 초기화를 해야하는 의무를 선택사항으로 만들 수 있다.
다만 초기화를 진행하지 않으면 undefined
가 나오게 된다.
Readonly
멤버 변수에 readonly
속성을 사용할 수도 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Greeter {
readonly name: string = "world";
constructor(otherName?: string) {
if (otherName !== undefined) {
this.name = otherName;
}
}
err() {
this.name = "not ok"; // Cannot assign to 'name' because it is a read-only property.
}
}
const g = new Greeter();
g.name = "also not ok"; // Cannot assign to 'name' because it is a read-only property.
readonly
속성이 주어진 멤버 변수는 클래스 내부에서도, 외부에서도 수정이 불가능한 상태가 된다.
Constructor
어느 객체지향 언어처럼 생성자 역시 지원한다.
1
2
3
4
5
6
7
8
9
10
class Point {
x: number;
y: number;
// Normal signature with defaults
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
}
}
constructor
라는 이름으로 클래스 내에서 함수를 정의하면 해당 함수는 생성자로서 취급받는다.
생성자란?
const point: Point = new Point();
처럼 new
연산자를 통해 Instance
를 생성할 때 실행되는 함수이다.
new Point(1, 2)
와 같이 파라미터를 넘겨주면 constructor
함수를 통해 받을 수 있다.
Constructor overloading
1
2
3
4
5
6
7
8
class Point {
// Overloads
constructor(x: number, y: string);
constructor(s: string);
constructor(xs: any, y?: any) {
// TBD
}
}
일반 멤버 함수와 마찬가지로 constructor
역시 Overloading
이 가능하다.
필요하다면 Union Type
도 사용할 수 있다.
Super call
다른 객체지향 언어를 써 본 사람이라면 역시 super
도 이해하기 쉬울 것이다.
클래스는 exntends
를 지원한다. 그렇다면 자식 클래스의 인스턴스를 생성하게 되면 부모 클래스의 constructor
는 언제 실행되는 걸까?
1
2
3
4
5
6
7
class Base {
constructor() {}
}
class Children extends Base {
constructor() {}
}
const childrenInstance: Children = new Children();
정확히 말하면 부모 클래스의 constructor
는 영원히 실행되지 않는다.
자식 클래스의 인스턴스를 생성한 것이지 부모의 인스턴스를 생성한 것이 아니기 때문이다.
다만 자식 클래스에서 부모 클래스의 생성자를 호출해야만 하는 상황이 생길 수가 있는데, 이럴 경우에 부모의 생성자를 호출하는 함수가 super
함수이다.
1
2
3
4
5
6
7
8
9
class Base {
k = 4;
}
class Derived extends Base {
constructor() {
super();
}
}
super()
형태로 사용하면 부모의 constructor
를 호출하게 된다. super
를 실행하는 타이밍을 조정하는 식으로 부모 constructor
의 실행 주기를 마음대로 할 수 있긴 하지만 가급적이면 constructor
의 제일 첫 줄에 선언하는게 베스트이다.
super
를 실행하지 않으면 부모의 멤버 변수를 참조할 수 없기 때문이다.
1
2
3
4
5
6
7
8
9
10
class Base {
k = 4;
}
class Derived extends Base {
constructor() {
console.log(this.k); // Error
super();
}
}
Getter
Javascript
에서 Array.length
를 수정해 본 적이 있으면 알겠지만 실제로 값을 재정의 할 때는 아무런 에러가 나오지 않는데도 값을 재정의 한 다음 출력을 해보면 내가 정의한 값이 아닌 올바른 값이 나오는 것을 볼 수 있다.
1
2
3
4
const arr = [1, 2, 3];
console.log(arr.length); // 3;
arr.length = 777; // OK
console.log(arr.length); // 3;
이런 Attribute
는 어떻게 만드는걸까?
답은 Getter
이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Stack<T> {
items: T[];
constructor() {
this.items = [];
}
public push(item: T) {
this.items.push(item);
}
public get length(): number {
return this.items.length;
}
}
const stack: Stack<number> = new Stack();
console.log(stack.length); // 0
stack.push(1);
console.log(stack.length); // 1
stack.length = 777; // Cannot assign to 'length' because it is a read-only property.(2540)
console.log(stack.length);
함수 앞에 get
접근자를 붙이면 해당 함수 이름으로 속성을 꺼낼 때 get
접근자 함수 안에 있는 로직에 따라 값을 꺼내오게 된다.
하지만 javascript
에서의 예제와는 다르게 read-only
이슈가 발생하여 수정이 되지는 않는다.
Setter
Getter
는 원하는 값을 실제로 존재하지 않는 멤버 변수명을 통해 얻어올 수 있는 방법임을 알았다. 그러면 Setter
도 대충 감이 올텐데, Setter
는 대입 연산자(=)를 통해 Attribute
에 값을 대입할 때, 정직하게 대입만 하는 것이 아닌 특정 로직을 거쳐서 값에 변조를 줄 때 사용한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
class Square {
x!: number;
y!: number;
public set square(area: number) {
this.x = Math.sqrt(area);
this.y = Math.sqrt(area);
}
}
const rect: Square = new Square();
console.log(rect.x, rect.y); // undefined undefined
rect.square = 25;
console.log(rect.x, rect.y); // 5, 5
정사각형의 넓이를 입력받아서 x
와 y
의 길이를 구하는 클래스이다.
다만 한가지 다른 점이 있다면, x
와 y
를 직접 초기화하지 않았음에도 불구하고 x
와 y
의 값이 설정 되었다는 점이다.
set
설정자 통해 함수를 정의하면 해당 함수 이름을 속성처럼 사용할 수 있다.
속성에 값을 대입하면 그 값을 parameter
로 넘기게 되고, set
함수를 실행하게 되는 것이다.
Index Signatures
여기서 일반 객체와 마찬가지로 Index Signatures
를 사용할 수 있다.
1
2
3
4
5
6
7
class MyClass {
[s: string]: boolean | ((s: string) => boolean);
check(s: string) {
return this[s] as boolean;
}
}
Class Heritage
Class
도 interface
를 통해 멤버들의 범위를 정해줄 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface Pingable {
ping(): void;
}
class Sonar implements Pingable {
ping() {
console.log("ping!");
}
}
class Ball implements Pingable {
pong() { // Error.
console.log("pong!");
}
}
Overriding
다른 객체지향 언어처럼 Overriding
을 지원한다.
부모 클래스가 갖고있는 멤버 함수와 동일한 이름으로 설정 하되, parameter
의 type
이나 갯수를 다르게 설정하는 방식으로 사용한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Base {
greet() {
console.log("Hello, world!");
}
}
class Derived extends Base {
greet(name?: string) {
if (name === undefined) {
super.greet();
} else {
console.log(`Hello, ${name.toUpperCase()}`);
}
}
}
const d = new Derived();
d.greet();
d.greet("reader");
super
키워드를 통해 부모의 메소드에 접근 할 수도 있다.
Relationship
A
클래스와 B
클래스가 서로 다르다는 것을 Typescript
는 어떻게 구분할까?
일반 객체와 마찬가지로 Class
구성하는 멤버들을 비교하여 구분한다.
1
2
3
4
5
6
7
8
9
10
11
12
class Point1 {
x = 0;
y = 0;
}
class Point2 {
x = 0;
y = 0;
}
// OK
const p: Point1 = new Point2();
그렇기 때문에 위 코드가 성립 된다.