TypeScript 클래스는 타입을 붙인 JavaScript 클래스입니다. 각 필드는 클래스 본문에 타입과 함께 선언하고, 메서드와 생성자는 타입이 있는 매개변수와 반환값을 받습니다.
컴파일된 출력은 타입이 제거된 평범한 JavaScript 클래스이므로, JavaScript 클래스의 모든 내용이 그대로 적용됩니다. TypeScript가 더하는 것은 컴파일 시점에 일어납니다. 마지막 호출은 프로그램이 실행되기 전에 거부됩니다.
필드 선언하기
생성자가 필드에 대입하려면 먼저 클래스 본문에 선언이 있어야 합니다. owner를 선언하지 않고 this.owner에 대입하면 오류 TS2339(속성이 존재하지 않음)입니다. 초기값이 있는 필드는 변수처럼 값에서 타입이 추론됩니다.
| 선언 | 의미 |
|---|---|
name: string; | 생성자에서 반드시 대입해야 함 |
count = 0; | 초기화됨, 타입은 number로 추론 |
label?: string; | 선택적, 타입은 string | undefined |
readonly id: number; | 한 번 대입한 뒤 읽기 전용 |
data!: string[]; | 확정 할당: 설정된다고 믿어 달라는 뜻 |
static total = 0; | 인스턴스가 아니라 클래스에 속함 |
strict가 켜져 있으면 strictPropertyInitialization이 선택적이지 않은 모든 필드가 값을 받는지 검사합니다. 이 클래스는 두 개를 빠뜨렸습니다.
컴파일러는 둘 다 보고합니다.
index.ts(3,5): error TS2564: Property 'name' has no initializer and is not definitely assigned in the constructor.
index.ts(4,5): error TS2564: Property 'age' has no initializer and is not definitely assigned in the constructor.
이 검사가 없으면 new Profile().name.toUpperCase()가 타입 검사를 통과한 뒤 undefined 때문에 멈출 것이기에 존재합니다. 각 필드는 네 가지 방법 중 하나로 고치세요. 초기값을 주거나, 생성자에서 대입하거나, ?로 선택적으로 표시하거나, 프레임워크나 init() 메서드가 나중에 설정한다면 name!: string이라고 씁니다. 마지막 방법은 그 필드의 검사를 끄므로 아껴 쓰세요.
메서드와 this
메서드는 함수처럼 타입을 붙입니다. 메서드 안에서 this는 인스턴스의 타입을 갖습니다. 메서드가 this를 반환하면 체이닝을 할 수 있고, 반환 타입은 하위 클래스를 자동으로 따라갑니다.
class QueryBuilder {
private parts: string[] = [];
where(condition: string): this {
this.parts.push(condition);
return this;
}
build(): string {
return this.parts.join(" AND ");
}
}
const sql = new QueryBuilder().where("age > 18").where("active = 1").build();
console.log(sql); // age > 18 AND active = 1
메서드를 객체에서 떼어 내면 this를 잃는데, JavaScript의 고전적인 버그입니다. TypeScript는 기본적으로 이를 잡지 못하지만, 메서드가 this 매개변수를 선언하면 잡습니다. 이 매개변수는 출력에서 지워지고 호출하는 곳만 지킵니다.
화살표 필드가 동작하는 것은 인스턴스마다 this를 캡처하는 자체 함수를 받기 때문입니다. 대가는 프로토타입에 공유되는 메서드 하나 대신 인스턴스마다 함수가 하나씩 생긴다는 것입니다.
getter와 setter
get과 set 접근자는 밖에서 보면 속성처럼 보입니다. setter 없는 getter는 읽기 전용이며, 대입하면 컴파일 오류(TS2540)입니다.
컴파일러가 대입을 거부하고, 검사를 우회하더라도 컴파일된 파일이 strict 모드로 실행되므로(컴파일러가 맨 위에 "use strict"를 출력합니다) 런타임에도 TypeError가 납니다. setter의 RangeError는 직접 작성한 런타임 검사입니다. number 타입은 값이 숫자라는 것만 말할 뿐, 범위에 대해서는 아무것도 모릅니다.
정적 멤버
static 필드와 메서드는 클래스 자체에 속합니다. 인스턴스가 아니라 클래스 이름으로 접근합니다. static { } 블록은 클래스가 정의될 때 한 번 실행됩니다.
정적 멤버는 클래스의 타입 매개변수를 쓸 수 없습니다. class Box<T>에서 static empty: T는 오류 TS2302입니다. 정적 필드는 하나뿐이고 모든 Box<string>, Box<number> 등이 공유하기 때문입니다.
인터페이스 구현하기
implements는 인터페이스가 요구하는 모든 것이 클래스에 있는지 검사하라고 컴파일러에 요청합니다. 빠진 멤버는 오류 TS2420: Class 'X' incorrectly implements interface 'Y'입니다.
클래스는 여러 인터페이스를 구현할 수 있습니다: class Doc implements Printable, Serializable. implements가 하지 않는 일이 두 가지 있습니다. 클래스 메서드의 매개변수 타입을 대신 지정해 주지 않습니다. check(s: string): boolean을 구현하는 클래스 안의 check(s) { ... }는 여전히 오류 TS7006(Parameter 's' implicitly has an 'any' type)이므로 타입을 표기하세요. 그리고 런타임에 흔적을 남기지 않으므로 obj instanceof Shape는 불가능합니다.
생성자의 private radius는 매개변수 속성으로, 필드 선언과 대입을 한 번에 합니다. 생성자를 보세요.
클래스는 타입이기도 하다
클래스 선언은 한 이름으로 두 가지를 만듭니다. 값(new로 호출하는 생성자 함수)과 타입(인스턴스의 모양)입니다. TypeScript는 타입을 구조로 비교하므로, 같은 public 멤버를 가진 객체라면 클래스로 만들지 않았더라도 그 타입에 맞습니다.
평범한 객체는 타입 검사를 통과하지만 런타임에는 Point가 아닙니다: ({ x: 6, y: 8 }) instanceof Point는 false입니다. private나 #private 멤버가 있는 클래스는 이를 막습니다. 그 클래스(또는 하위 클래스)의 인스턴스만 대입할 수 있습니다.
흔한 실수
- 필드를 선언하고 대입하지 않기.
TS2564는 그 필드가undefined일 것이라고 알려 주는 것입니다.!로 입막음하기보다 초기화하세요. - 메서드를 콜백으로 넘기기.
button.onclick = obj.handle은this를 잃습니다. 화살표 함수 필드나obj.handle.bind(obj)를 쓰세요. implements가 코드나 타입을 더해 줄 거라 기대하기. 검사만 합니다. 매개변수 타입에는 여전히 표기가 필요합니다.- 인터페이스에
instanceof쓰기. 인터페이스는 런타임에 존재하지 않습니다. 클래스를 확인하거나 타입 가드를 쓰세요. - 타입이 데이터를 검증한다고 가정하기.
number필드는 컴파일 시점에 어떤 숫자든 받고, 런타임에는 타입 없는 데이터로부터 무엇이든 받습니다.
자주 묻는 질문
TypeScript에서 클래스는 어떻게 만드나요?
클래스 본문에 각 필드를 타입과 함께 선언하고, 생성자와 메서드는 JavaScript처럼 씁니다: class User { name: string; constructor(name: string) { this.name = name; } greet(): string { return "Hi, " + this.name; } }. 인스턴스는 new User("Ada")로 만듭니다.
"has no initializer and is not definitely assigned in the constructor"는 무슨 뜻인가요?
strictPropertyInitialization(strict에 포함)에서 나오는 오류 TS2564입니다. string 타입 필드를 아무것도 설정하지 않으면 undefined로 시작할 것이라는 뜻입니다. 초기값(name = ""), 생성자에서의 대입, 선택적 필드(name?: string)로 고치거나, 생성자 밖에서 무언가가 설정한다면 확정 할당 단언(name!: string)을 쓰세요.
TypeScript에서 implements와 extends의 차이는 무엇인가요?
extends는 부모 클래스의 코드를 상속합니다. 부모의 필드와 메서드가 런타임에 자식에게 존재합니다. implements는 클래스가 인터페이스의 모양을 갖는지 컴파일러에 검사를 요청할 뿐입니다. 클래스에 아무것도 더하지 않고, 컴파일된 JavaScript에서 사라집니다.
TypeScript에서 클래스를 타입으로 쓸 수 있나요?
네. 클래스 이름은 값(생성자)이면서 타입(인스턴스의 모양)입니다. TypeScript는 타입을 구조로 비교하므로 let u: User는 같은 public 멤버를 가진 어떤 객체든 받습니다. typeof User는 생성자 자체의 타입입니다.
TypeScript 클래스는 런타임에 JavaScript 클래스와 다른가요?
아니요. TypeScript는 클래스를 평범한 JavaScript 클래스로 컴파일합니다. 타입 표기, implements 절, private 같은 제한자는 지워지고, #private 필드, static 블록, getter 같은 JavaScript 기능만 런타임에 존재합니다.