일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 |
- 5014번
- 1969번
- 5427번
- javascript
- 풀이
- 타입스크립트
- 백준
- 14940번
- 1541번
- 1926번
- 13913번
- 한윤정이 이탈리아에 가서 아이스크림을 사먹는데
- 토마토
- 6593번
- 16953번
- 123만들기
- 알고리즘
- 20300번
- 타입스크립트 프로그래밍
- 16439번
- 2422번
- node.js
- 정리
- 2503번
- 7526번
- 맥주마시면서걸어가기
- 나이트의이동
- 자바스크립트
- 17626번
- 20365번
- Today
- Total
Sqsung DevLog
<타입스크립트 프로그래밍> 5장: 클래스와 인터페이스 정리 본문
5장: 클래스와 인터페이스
5-1. 클래스와 상속
타입스크립트는 클래스의 프로퍼티와 메서드에 3 가지 접근 한정자를 제공한다
- public: 어디에서나 접근할 수 있음 (default)
- protected: 이 클래스와 서브클래스의 인스턴스에서만 접근 가능함
- private: 이 클래스의 인스턴스에서만 접근할 수 있음
type Color = 'Black' | 'White'; type File = 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H'; type Rank = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8; /** * @private 접근 한정자는 자동으로 매개변수를 this에 할당하며 (eg. file => this.file) 가시성은 private으로 한다 */ class Position { constructor( private file: File, private rank: Rank ) { } } class Piece { protected position: Position // 마찬가지로 this에 할당, 하지만 Piece 및 서브클래스 인스턴스에게 모두 접근 허용 constructor( prviate readonly color: Color, // 클래스 인스턴스만 접근 가능하며 변경 불가능 file: File, rank: Rank, ) { this.position = new Position(file, rank); } }
abstract
키워드를 사용하면 사용자가 인스턴스를 직접 생성하지 못하게 막고, 클래스를 상속받은 클래스를 통해서만 인스턴스화할 수 있도록 허용할 수 있다abstract class Piece { constructor( //... ) } // 직접 인스턴스를 생성하려고 하면 에러가 발생한다 new Piece('White', 'E', 1); // Error: 추상 클래스의 인스턴스는 생성할 수 없음
또한, 필요한 메서드를 추상 클래스에 자유롭게 추가할 수 있다
아래 예제의 경우
canMoveTo
라는 메서드를 주어진 시그니처와 호환되도록 구현해야 함을 하위 클래스에 알리는 것이다abstract class Piece { moveTo(position: Position) { this.position = position; } // *Piece를 상속받았으나, canMoveTo 메서드 구현 안하면 컴파일 타임에 타입 에러 발생 abstract canMoveTo(position: Position): boolean }
Piece를 상속받은 하위 클래스이자,
canMoveTo
를 가지고 있는 King 클래스를 만들면 아래와 같다class Position { // ... distanceFrom(position: Position) { return { rank: Math.abs(position.rank - this.rank); file: Math.abs(position.file.charCodeAt(0) - this.file.charCodeAt(0)); } } } class King extends Piece { canMoveTo(position: Position) { let distance = this.position.distanceFrom(position); return distance.rank < 2 && ditsance.file < 2; } }
타입스크립트에서의 클래스를 요약하자면 아래와 같다 :
class 키워드로 클래스를 선언한 후 extends 키워드로 다른 클래스를 상속 받을 수 있다
구체 클래스와 추상 클래스가 존재하며, 추상 클래스의 경우 추상 메서드와 추상 프로퍼티를 가질 수 있다
메서드는 default로 public이지만, private, protected, public 중 한 가지의 한정자를 가질 수 있다. 또한, 메서드는 인스턴스 메서드와 정적 메서드 두 가지로 구분된다
💡 인스턴스 vs. 정적 메서드 1. 정적 메서드는 인스턴스를 생성하지 않아도 사용 가능한 메서드 (예: Array.isArray(arr)) 2. 인스턴스 메서드는 class prototype에 소속되어 모든 인스턴스에게 상속되며, 인스턴스를 통해 사용해야 함 (예: arr.push(4))
클래스는 인스턴스 프로퍼티도 가질 수 있으며, 이 프로퍼티들은 private, protected, public 중 한 가지 한정자를 갖는다 (마찬가지로 public이 기본값). constructor의 매개변수나 프로퍼티 초기자에도 이들 한정자를 사용할 수 있다.
인스턴스 프로퍼티를 선언할 때 readonly를 추가할 수 있다
5-2. super
- constructor에서만 호출할 수 있는
super()
특별한 타입의 생성자 호출은 자식 클래스에 생성자 함수가 있으면 무조건 호출해야 한다 (안하면 부모 클래스와 정상적으로 연결되지 않음) - 또한, 자식 인스턴스는
super
를 이용해 부모 버전의 메서드 호출할 수 있다- 예를 들어 자식, 부모 클래스 모두
getName
이라는 메서드를 가지고 있다고 치면, 자식 클래스의getName
메서드가 부모의getName
메서드를 overriding 하지만,super.getName
으로 호출하면 부모 버전의 메서드를 사용할 수 있다 - 단, super 키워드로 부모의 메서드에는 접근할 수 있지만 프로퍼티에는 접근할 수 없다
- 예를 들어 자식, 부모 클래스 모두
5-3. this를 반환 타입으로 사용하기
this를 값뿐 아니라 타입으로도 사용할 수 있다
클래스를 정의할 때라면 메서드의 반환 타입을 지정할 때 this 타입을 유용하게 사용할 수 있다
예를 들어 간단한
Set
클래스를 만들어보면 아래와 같을 것이다class Set { has(value: number): boolean { // has: Set 인스턴스가 특정 값을 가지고 있는지 확인하는 인스턴스 메서드 } add(value: number): Set { // add: Set 인스턴스에 특정 값을 추가한 후, 변경된 Set 인스턴스 반환하는 메서드 } }
Set
클래스를 상속받는MutableSet
서브클래스를 구현한다고 치면,has
메서드는 문제 없이 상속받을 수 있지만,add
메서드의 경우 Set 인스턴스를 반환해야 한다. 서브클래스에서add
메서드를 현재 상태 그대로 상속받아 사용하면MutableSet
대신Set
인스턴스를 반환햔다.MutableSet
에서는 새로운add
메서드로 오버라이드해야 한다.class MutableSet extends Set { delete(value: number): boolean { // MutableSet 인스턴스만 가지고 있는 delete 메서드 } add(value: number): MutableSet { // Set으로부터 상속받은 add 메서드 오버라이드 } }
하지만 이렇게 하면 모든 서브클래스는 this를 반환하는 모든 메서드의 시그니처를 오버라이드해야 하므로, 클래스와 상속구조를 이용하는 것의 의미가 없어졌다
대신, 부모클래스에서 반환 타입을 this로 지정하면 이 번거로운 작업을 타입스크립트가 알아서 해준다
class Set { has(value: number): boolean { // ... } add(value: number): this { // Set의 this는 Set 인스턴스를, MutableSet의 this는 MutableSet의 인스턴스를 자동으로 가리킴 // 즉, MutableSet에서 add 메서드를 오버라이드할 필요 없어짐 } }
5-4. 인터페이스
클래스는 주로 인터페이스를 통해 사용할 때가 많다
type
키워드처럼 인터페이스도 타입에 이름을 지어주는 수단이지만, 인터페이스를 사용하면 타입을 더 깔끔하게 정의할 수 있다타입과 인터페이스의 차이는 다음과 같다
- 타입 별칭의 오른편에는 타입 표현식(타입, 그리고 &, | 등의 타입 연산자)를 포함한 모든 타입이 등장할 수 있지만, 인터페이스의 오른 편에는 반드시 형태가 나와야 한다.
- 인터페이스를 상속할 때 타입스크립트는 상속받는 인터페이스의 타입에 상위 인터페이스를 할당할 수 있는지 확인한다
interface A { good(x: number): string bad(x: number): string } interface B extends A { good(x: string | number): string bad(x: string): string // Error: number 타입은 string 타입에 할당할 수 없음 }
- 이름과 범위가 같은 인터페이스가 여러 개 있다면 이들이 자동으로 합쳐진다 (선언 합침)
5-4-1. 선언 합침
선언 합침declaration merging은 컴파일러가 같은 이름으로 선언된 개별적인 선언 두 개를 하나의 정의로 합치는 것을 의미한다
// 1. User 라는 interface는 한 개의 필드('name')을 가지고 있음 interface User { name: string } // 2. 이제 User라는 interface는 두 개의 필드('name, age')를 가지고 있음 interface User { age: number } // 3. User가 선언 합침되었기 때문에 에러가 발생하지 않음 let a: User = { name: 'James', age: 29 }
같은 로직을
type
키워드를 사용해서 표현하면 충돌이 일어나게 된다type User = { name: string }; type User = { age: number }; // Error: 중복된 식별자 'User'
선언 합침을 통해 동명의 인터페이스를 사용하더라도, 프로퍼티의 타입 간의 충돌이 일어나면 에러가 발생한다
interface User { age: string }; interface User { age: number }; // Error: 프로퍼티 age는 반드시 string 타입이어야 됨
또한 제네릭을 선언한 인터페이스들의 경우, 제네릭들의 선언 방법과 이름까지 똑같아야 합칠 수 있다
interface User<Age extends number> { age: Age } interface User<Age extends string> { age: Age } // Error: User의 모든 선언은 같은 타입 매개변수를 가져야 함
5-4-2. 구현
클래스를 선언할 때
implements
라는 키워드를 이용해 특정 인터페이스를 만족 시킴을 표현할 수 있다interface Animal { eat(food: string): void sleep(hours: number): void } class Cat implements Animal { eat(food: string) { console.info(`Ate some ${food}, Mmm!`); } sleep(hours: number) { console.info(`Slept for ${hours} hours!`); } }
Cat은 Animal이 선언하는 모든 메서드를 구현해야 하며, 필요하면 메서드나 프로퍼티를 추가로 구현할 수 있다
인터페이스로 인스턴스 프로퍼티를 정의할 수 있지만, 가시성 한정자(private, protected, public)는 선언할 수 없으며, static 키워드도 사용할 수 없다. readonly로 설정할 수는 있다.
또한, 한 클래스가 하나의 인터페이스만 구현할 수 있는 것은 아니므로 필요하면 여러 인터페이스를 구현할 수 있다
interface Animal { readonly name: string, eat(food: string): void, sleep(hours: number): void } interface Feline { meow(): void } class Cat implements Animal, Feline { name = 'Whiskers' eat(food: string) { console.info(`Ate some ${food}, Mmm!`); } sleep(hours: number) { console.info(`Slept for ${hours} hours!`); } meow() { console.info('Meowwww!'); } }
5-4-3. 인터페이스 구현 vs. 추상 클래스 상속
- 인터페이스는 아무런 자바스크립트 코드를 만들지 않으며, 컴파일 타임에만 존재하지만, 추상 클래스는 런타임의 자바스크립트 클래스 코드를 만든다
- 반면 추상 클래스는 기능이 더 풍부하다. 예로, 생성자 함수와 기본 구형늘 가질 수 있으며, 프로퍼티와 메서드에 접근 한정자를 지정할 수 있다 (모두 인터페이스에서는 제공되지 않음)
- 여러 클래스에서 공유하는 구현이라면 추상 클래스를 사용하고, 가볍게 클래스의 형태를 정의할 때는 인터페이스를 사용하는 것이 좋다
5-5. 클래스는 구조 기반 타입을 지원한다
타입스크립트는 클래스를 비교할 때 다른 타입과 달리 이름이 아니라 구조를 기준으로 삼는다
class Zebra { trot() { /* ... */ } } class Poodle { trot() { /* ... */ } } function ambleAround(animal: Zebra) { animal.trot(); } const zebra = new Zebra; const poodle = new Poodle; /** * 타입스크립트 입장에서는 Zebra, Poodle 두 클래스 모두 `.trot`을 구현하며 서로 호환된다 * Zebra 클래스를 받는 `ambleAround` 함수에 Zebra 대신 Poodle을 전달해도 아무 문제 없다 */ ambleAround(zebra); // OK ambleAround(poodle); // OK
단, 클래스에 private/protected 필드가 있고, 할당하려는 클래스 및 서브클래스의 인스턴스가 아니라면 할당할 수 없다고 판정한다
class A { private x = 1 } class B extends A { // ... } function randomFunc(param: A) { // ... } randomFunc(new A); // OK randomFunc(new B); // OK // 'A'의 프로퍼티는 privte 이지만 { x: number }는 private이 아니므로 실패 randomFunc({ x: 1}) // Error!
5-6. 클래스는 값과 타입을 모두 선언한다
타입스크립트에서 값과 타입은 별도의 네임스페이스에 존재한다
사용 방법에 따라 타입스크립트가 알아서 값 또는 타입으로 해석한다
if (typeOrValue + 1 > 3); // 값으로 추론 let x: typeOrValue = 3; // 타입으로 추론
하지만 클래스와 열거형은 특별하게 값 네임스페이스에 값을, 타입 네임스페이스에 타입을 동시 생성한다
class C { /* ... */ } let c: C = new C; enum E { F, G} let e: E = E.F
5-7. 다형성
함수와 타입처럼, 클래스와 인터페이스도 기본값과 상한/하한 설정을 포함한 다양한 제네릭 타입 매개변수 기능을 지원한다
제네릭 타입의 범위는 클래스나 인터페이스가 되게 할 수도 있고 특정 메서드로 한정할 수도 있다
// 1. class와 제네릭을 선언했으므로 클래스 전체에서 타입을 사용할 수 있다 (MyMap의 모든 메서드 및 프로퍼티) class MyMap<K, V> { // 2. 생성자 함수에는 제네릭 타입을 선언할 수 없다. constructor 대신 class 선언에 사용해야 한다 constructor(initialKey: K, initialValue: V) { // ... } // 3. class로 한정된 타입은 클래스 내부의 어디에서나 사용 가능하다 get(key: K): V { // ... } set(key: K, value: V): void { // ... } // 4. 인스턴스 메서드는 클래스 수준의 제네릭을 사용할 수 있으며, 자신만의 제네릭 타입 (V1, K1)을 추가로 선언할 수 있다 merge<K1, V1>(map: MyMap<K1, V1>): MyMap<K | K1, V | V1> { // ... } // 5. 정적 메서드는 class 수준의 제네릭을 사용할 수 없다 // 5a. of는 1에서 선언한 K, V에 접근할 수 없고 자신만의 K, V를 직접 선언했다 static of<K, V>(k: K, v: V): MyMap<K, V> { // ... } }
새로운 인스턴스를 생성할 때는 제네릭에 구체 타입을 명시하거나, 타입스크립트가 타입을 추론하도록 할 수 있다
let a = new MyMap<string, number>('k', 1); // 명시: MyMap<string, number> let b = new MyMap('k', true); // 추론: MyMap<string, boolean>
인터페이스에서도 제네릭을 사용할 수 있다
interface MyMap<K, V> { get(key: K): V set(key: K, value: V): void }
5-8. 믹스인
- 믹스인mixin은 클래스 A가 클래스 B를 확장해서 기능을 상속받는 것이 아니라, 함수 B가 클래스 A를 받고 기능이 추가된 새로운 클래스를 반환하는 것이다.
💡 믹스인 (mixin)
1. 생성자(constructor)를 받음
2. 생성자를 확장하여 새 기능 추가한 클래스 생성
3. 새 클래스 반환
// 1. 모든 생성자를 표현하는 ClassConstructor 타입 선언
type ClassConstructor = new (...args: any[]) => {};
/**
* 2. 한 개의 매개변수 C만 받는 withEZDebug 믹스인 함수
* extends로 강제했듯이 C는 최소한 클래스 생성자여야 한다
*/
function withEZDebug<C extends ClassConstructor>(Class: C) {
// 3. 생성자를 인수로 받아 생성자를 반환하는 함수이므로 익명 클래스 생성자 반환
return class extends Class {
// 4. 매개변수로 받는 클래스가 받는 인수는 다 받을 수 있어야 한다
constructor(...args: any[]) {
// 5. 익명 클래스는 다른 클래스를 상속받으므로 Class의 생성자를 호출해야 함
super(...args)
}
}
}
// * constructor 로직이 따로 없는 경우 4, 5번 스텝은 넘어가도 된다
5-10. final 클래스 흉내내기
객체지향언어들에서
final
키워드는 클래스나 메서드를 확장하거나 오버라이드할 수 없게 만드는 기능이다타입스크립트에서는 비공개 생성자(private constructor)로 final 클래스를 흉내낼 수 있다
생성자를 private으로 선언하면 new로 인스턴스를 생성하거나 클래스를 확장할 수 없게 된다
class MesageQueue { private constructor(private messages: string[]) { /* ... */ } }
하지만 이렇게 constructor만 private으로 선언하는 경우, 상속을 막는 것은 물론 인스턴스 생성도 불가해진다 (반대로 final 키워드를 사용하면 상속만 막을 뿐, 인스턴스는 정상 생성 가능하다)
이를 위해 해당 클래스를 반환하는 새로운 정적 메서드를 구현해두면 된다
class MessageQueue { private constructor(private messages: string[]) { /* ... */ } static create(messages: string[]) { return new MessageQueue(messages); } } MessageQueue.create([]); // MessageQueue class BadQueue extends MessageQueue { } // Error: MessageQueue 클래스를 확장할 수 없음
5-11. 디자인 패턴
5-11-1. 팩토리 패턴
팩토리 패턴은 어떤 객체를 만들지 전적으로 팩토리에 위임한다
type Shoe = { purpose: string } class BalletFlat implements Shoe { purpose = 'dancing' } class Boot implements Shoe { purpose = 'woodcutting' } class Sneaker implements Shoe { purpose = 'walking' } let Shoe = { create(type: 'balletFlat' | 'boot' | 'sneaker'): Shoe { swtich(type) { case 'balletFlat': return new BalletFlat case 'boot': return new Boot case 'sneaker': return new Sneaker } } }
Shoe
팩토리를 사용하기 위해서는 단순히.create
함수를 호출하면 된다Shoe.create('boot');
5-11-2. 빌더 패턴
빌더 패턴으로 객체의 생성과 객체 구현 방식을 분리할 수 있다
class RequestBuilder { private url: string | null = null private method: 'get' | 'post' | null = null private data: object | null = null setUrl(url: string): this { this.url = url; return this; } setMethod(method: 'get' | 'post'): this { this.method = method; return this; } setData(data: object): this { this.data = data; return this; } }
'TypeScript' 카테고리의 다른 글
<타입스크립트 프로그래밍> 7장: 에러 처리 - 정리 (0) | 2023.06.05 |
---|---|
<타입스크립트 프로그래밍> 6장: 고급 타입 - 정리 (0) | 2023.06.01 |
<타입스크립트 프로그래밍> 4장: 함수 (0) | 2023.05.27 |
<타입스크립트 프로그래밍> 3장: 타입의 모든 것 (0) | 2023.05.26 |
<타입스크립트 프로그래밍> 2장: 타입스크립트 3,000미터 상공에서 내려다보기 (0) | 2023.05.26 |