[Learning Typescript] Ch.7 클래스

Typescript
Dragon C's avatar
Jan 10, 2024
[Learning Typescript] Ch.7 클래스

8.1 클래스 메서드

  • 타입 스크립트는 독립 함수(standalone function)를 이해하는 것과 동일한 방식으로 메서드를 이해함

  • 매개변수에 타입이나 기본 값을 지정하지 않으면 기본으로 any 타입

  • 메서드 호출 시 허용 가능한 수의 인수가 필요

  • 재귀 함수가 아니라면 대부분 반환 타입을 유추할 수 있음

  • 클래스 생성자(constructor)도 전형적인 클래스 메서드 처럼 취급됨

class Greeted {
	constructor(message: string) {
		console.log(`As I always say: ${message}!`);
	}
}

new Greeted("take chances, make mistakes, get message");

new Greeted();
// Error: Expected 1 argument, but got 0.

8.2 클래스 속성

  • 클래스의 속성을 읽거나 쓰려면 클래스에 명시적으로 선언해야 함

  • 클래스 속성 이름 뒤에 선택적으로 타입 애너테이션이 붙음

  • 타입스크립트는 생성자 내에서 할당을 통해 멤버를 자동으로 추론하지 않고 명시적 선언을 통해 찾음

  • 클래스 인스턴스가 사용될 때 코드에서 클래스 인스턴스에 존재하지 않는 멤버에 접근 시도할 경우 타입스크립트는 오류를 발생시킴

8.2.1 함수 속성

  • 클래스의 멤버를 호출 가능한 함수로 선언하는 두 가지 구문이 있음

  1. 메서드 접근 방식

    • 멤버 이름 뒤에 괄호를 붙임 ex. myMethod(){}

    • 함수를 클래스 프로토타입에 할당하므로 모든 클래스 인스턴스는 동일한 함수 정의를 사용함

  2. 값이 함수인 속성을 선언하는 방식

    • 멤버의 속성을 함수 타입으로 선언함 ex. myProperty: () ⇒ {}

    • 클래스의 인스턴스당 새로운 함수가 생성됨

    • 항상 클래스 인스턴스를 가리키도록 하는 () ⇒ 화살표 함수와 함께 사용될 수 있다는 유용함이 있으나 클래스 인스턴스마다 새로운 함수를 만들어야 하므로 시간과 메모리 비용이 들 수 있음 (= 화살표 함수를 사용할 때, 함수 내에서 this가 항상 클래스 인스턴스를 가리키도록 하려면 각 인스턴스마다 새로운 함수를 생성해야 하는데 실행 시간과 메모리 사용량이 증가하는 비용이 따르게 됨을 의미)

    • 매개변수와 반환 타입도 지정할 수 있음

8.2.2 초기화 검사

  • 엄격한 컴파일러 설정이 활성화된 상태에서는 undefined 가 타입 선언에 포함되지 않은 각 속성에 대해 생성자에서 값이 할당되었는지 확인함

  • 엄격한 초기화 검사가 수행되지 않을 경우 값이 할당되지 않아도 올바르게 컴파일은 되지만 런타임 시 오류가 발생함

  • 확실하게 할당된 속성

    • 엄격한 초기화 검사를 적용하면 안되는 속성의 경우 이름 뒤에 !를 추가해 검사를 비활성화 할 수 있음

    • 이 경우 타입스크립트에 속성이 처음 사용되기 전 undefined가 할당됨

    • !어서션을 추가하여 타입 안정성을 줄이기 보다는 사용하지 않는 방향으로 리팩토링할 것을 권장함

8.2.3 선택적 속성

  • 속성의 이름 뒤에 ?를 추가하여 속성을 옵션으로 선언할 수 있음

  • 생성자에서 선택적 속성을 명시적으로 설정하지 않아도 됨

8.2.4 읽기 전용 속성

  • 선언된 속성 이름 앞에 readonly 키워드를 추가해 속성을 읽기 전용으로 선언할 수 있음

  • 타입 시스템에만 존재하는 키워드로 자바스크립트로 컴파일 시 삭제됨

  • 선언된 위치 또는 생성자에서 초깃값만 할당할 수 있음

  • 원시 타입의 초깃값을 갖는 readonly 선언 속성의 경우 리터럴 타입으로 유추되므로 생성자에서 값을 할당하기 위해서는 속성에 타입 애너테이션으로 타입을 지정해주어야 함

8.3 타입으로서의 클래스

  • 클래스 선언은 런타임 값을 생성하는 동시에 (클래스 자체) 타입을 생성하여 타입 주석에서 사용할 수 있음 즉, 클래스는 런타임과 타입 시스템 양쪽에서 동시에 역할을 한다는 점에서 상태적으로 독특함

  • 클래스의 동일한 멤버를 모두 포함하는 모든 객체 타입을 클래스에 할당할 수 있는 것으로 간주함

  • withSchoolBus는 SchoolBus 타입의 매개변수를 받음. 매개변수로 SchoolBus 클래스 인스턴스처럼 타입이 () ⇒ string[]인 getAbilities 속성을 가진 모든 객체를 할당할 수 있음 (자주 사용되는 방식은 아님)

class SchoolBus {
	getAbilities() {
		return ["magic", "shapeshifting"];
	}
}

function withSchoolBus(bus: SchoolBus) {
	console.log(bus.getAbilities());
}

withSchoolBus(new SchoolBus()); // Ok

// Ok
withSchoolBus({
	getAbilities: () => ["transmogrification"],
});

withSchoolBus({
	getAbilities: () => 123,
	// Error: Type 'number' is not assignable to type 'string[]'
})

8.4 클래스와 인터페이스

  • 클래스 이름 뒤에 implements 키워드와 인터페이스 이름을 추가함으로써 클래스의 해당 인스턴스가 인터페이스를 준수한다고 선언할 수 있음 → 해당 클래스의 인스턴스가 인터페이스에 할당 가능해야 한다는 것을 나타냄. 어떠한 불일치가 있다면 타입 체크 도구에서 타입 오류로 표시됨.

interface Learner {
	name: string;
	study(hours: number): void;
}
class Student implements Learner {
	name: string;
	constructor(name: string) {
		this.name = name;
	}
	study(hours: number) {
		for (let i = 0; i < hours; i+= 1) {
			console.log("...studying...");
		}
	}
}

class Slacker implements Learner {
// Error: Class 'Slacker' incorrectly implements interface 'Learner'.
// Property 'study' is missing in type 'Slacker'
// but required in type 'Learner'.
name = "Rocky";
}
  • 타입스크립트는 인터페이스에서 클래스의 메서드 또는 속성 타입을 유추하지 않음. 위의 예제에서 Slacker 클래스에 study(hours){} 메서드를 추가했을 경우 타입스크립트는 타입 에너테이션을 지정하지 않는 한 hours 매개변수를 any로 간주함

  • 인터페이스를 구현하는 것은 순전히 안정성 검사를 위해서임

  • 인터페이스의 멤버를 클래스 정의로 복사하지 않으나 인터페이스 구현 시 클래스 인스턴스가 사용되는 곳에서가 아닌 클래스 정의에서 타입 체커에 의도를 알리고 타입 오류를 나타내기 위함임

8.4.1 다중 인터페이스 구현

  • 클래스는 다중 인터페이스를 구현해 선언할 수 있음

  • 인터페이스 이름 사이에 쉼표를 넣어 구분하고 개수 제한은 없음

class ReportCard implements Graded, Reporter {

}
  • 구현하고자 하는 모든 인터페이스의 속성을 가지도록 해야 타입 오류가 발생하지 않음

  • 두 개의 충돌하는 인터페이스를 구현하는 클래스를 선언하려고 할 때는 클래스에 타입 오류가 발생함

interface AgeIsANumber {
	age: number;
}
interface AgeIsNotANumber {
	age: () => string;
}
class AsNumber implements AgeIsANumber, AgeIsNotANumber {
	age = 0;
	// Error: Property 'age' in type 'AsNumber' is not assignable
	// to the same property in base type 'AgeIsNotANumber'.
	// Type 'number' is not assignable to type '() => string'.
}
class NotAsNumber implements AgeIsANumber, AgeIsNotANumber {
	age() { return ""; }
	// Error: Property 'age' in type 'NotAsNumber' is not assignable
	// to the same property in base type 'AgeIsANumber'.
	// Type '() => string' is not assignable to type 'number'.
}

8.5 클래스 확장

  • 다른 클래스를 확장하거나 하위 클래스를 만드는 개념에도 타입 검사를 추가함

  • 기본 클래스에 선언된 모든 메서드와 속성은 파생 클래스(하위 클래스)에서 사용 가능함

class Teacher {
	teach() { ... }
}

class StudentTeacher extends Teacher {
	learn() { ... }
}

const teacher = new StudentTeacher();
teacher.teach(); // Ok
teacher.learn(); // Ok

8.5.1 할당 가능성 확장

  • 하위 클래스도 기본 클래스의 멤버를 상속함

  • 하위 클래스의 인스턴스는 기본 클래스의 모든 멤버를 가지므로 기본 클래스의 인스턴스가 필요한 모든 곳에서 사용할 수 있음

  • 반대로, 기본 클래스에 하위 클래스가 가지고 있는 모든 멤버가 없다면 하위 클래스가 필요할 때 기본 클래스를 사용할 수 없음

    • 하위 클래스의 선택적 속성은 오류로 처리하지 않음

8.5.2 재정의된 생성자

  • 하위 클래스에서 자체 생성자를 정의하지 않으면 암묵적으로 기본 클래스의 생성자를 사용함

  • 자체 생성자를 선언할 경우 super 키워드를 사용해 기본 클래스의 생성자를 호출해야 함

  • 기본 클래스의 생성자를 호출하기 전에 this 혹은 super에 접근하려고 하는 경우 타입 오류를 보고함

8.5.3 재정의된 메서드

  • 하위 클래스에서 기본 클래스의 메서드와 동일한 이름으로 새 메서드를 다시 선언할 수 있음

  • 주의할 점은 기본 클래스를 사용하는 모든 곳에 하위 클래스를 사용할 수 있으므로 재정의된 메서드의 타입도 기본 메서드 대신 사용할 수 있어야 함

8.5.4 재정의된 속성

  • 하위 클래스에서 기본 클래스의 속성과 동일한 이름으로 속성을 다시 선언할 수 있음

  • 하위 클래스는 기본 클래스와 구조적으로 일치해야 하고 속성을 다시 선언하는 경우 해당 속성을 유니언 타입의 더 구체적인 하위 집합이나 기본 클래스 속성 타입에서 확장되는 타입으로 정의함

  • 예를 들어, 기본 클래스의 속성이 number | undefined 타입일 때, 하위 클래스에서 동일한 이름의 속성을 number 타입으로 선언하는 것은 가능함

  • 그러나 기본 클래스의 속성이 number 타입인데, 하위 클래스에서 동일한 이름의 속성을 number | string 타입으로 선언하는 것은 타입 오류임

8.6 추상 클래스

  • 일부 메서드의 구현을 선언하지 않고, 하위 클래스가 해당 메서드를 제공할 것을 예상해 기본 클래스를 만들 수도 있음

  • 추상화하려는 클래스 이름과 메서드 앞에 abstract 키워드를 추가하면 됨

abstract class School {
	readonly name: string;

	constructor(name: string) {
		this.name = name;
	}

	abstract getStudentType(): string[];
}

class Preschool extends School {
	getStudentTypes() {
		return ["preschooler"];
	}
}

class Absence extends School { }
// Error: Nonabstract class 'Absence' does not implement
// inherited abstract member 'getStudentTypes' from class 'School'
  • 추상 클래스는 직접 인스턴스화 할 수 없고 추상 클래스가 아닌 하위 클래스를 사용해야 함

8.7 멤버 접근성

  • 자바스크립트에서는 클래스 멤버 이름 앞에 #을 추가해 private 클래스 멤버임을 나타냄

  • private 클래스 멤버는 해당 클래스 인스턴스에서만 접근 가능함

  • 타입스크립트의 클래스 지원은 자바스크립트의 # 프라이버시보다 먼저 만들어졌음

  • 타입스크립트는 private 클래스 멤버를 지원하지만, 타입 시스템에만 존재하는 클래스 메서드와 속성에 대해 조금 더 미묘한 프라이버시 정의 집합을 허용함(세밀한 차이가 있을 수 있음을 말함)

  • 키워드

    • public (기본값): 모든 곳에서 누구나 접근 가능

    • protected: 클래스 내부 또는 하위 클래스에서만 접근 가능

    • private: 클래스 내부에서만 접근 가능

  • 키워드는 타입 시스템 내에만 존재하며 자바스크립트로 컴파일되면 키워드는 제거됨

  • 타입스크립트의 멤버 접근성은 타입 시스템에서만 존재하지만 자바스크립트의 private 선언(#)은 런타임에도 존재한다는 차이가 있음

class Base {
	isPublicImplicit = 0;
	public isPublicExplicit = 1;
	protected isProtected = 2;
	private isPrivate = 3;
	#truePrivate = 4;
}

class Subclass extends Base {
	examples() {
		this.isPublicImplicit; // Ok
		this.isPublicExplicit; // Ok
		this.isProtected; // Ok
		
		this.isPrivate;
		// Error: Property 'isPrivate' is private
		// and only accessible within class 'Base'.
		
		this.#truePrivate;
		// Property '#truePrivate' is not accessible outside
		// class 'Base' because it has a private identifier.
	}
}

new Subclass().isPublicImplicit; // Ok
new Subclass().isPublicExplicit; // Ok

new Subclass().isProtected;
// ~~~~~~~~~~~
// Error: Property 'isProtected' is protected
// and only accessible within class 'Base' and its subclasses.

new Subclass().isPrivate;
// ~~~~~~~~~~~
// Error: Property 'isPrivate' is private
// and only accessible within class 'Base'.

Share article

Typescript Study