클래스메서드
//1.
class Greeter {
greet(name: string) {
console.log('${name}, do your stuff!');
}
}
new Greeter().greet('Miss Frizzle');
new Greeter().greet();
// Error: Expected 1 arguments, but got 0.
- string 타입의 단일필수 매개변수를 갖는 greet 클래스 메서드를 가진 Greeter 클래스를 정의함.
//2.
class Greeted {
constructor(message: string) {
console.log('As I always say: ${message}!')
}
}
new Greeted('take chances, make mistakes, get messy');
new Greeted();
//Error: Expected 1 arguments, but got 0.
- 클래스 생성자constructor는 매개변수와 관련하여 전형적인 클래스 메서드처럼 취급된다
- 타입스크립트는 메서드 호출 시 올바른 타입의 인수가 올바른 수로 제공되는지 확인하기 위해 타입검사를 수행함.
- Greeted 생성자는 message: string으로 매개변수가 제공되어야 한다.
클래스속성
//1.
class FieldTrip {
destination: string;
constructor(destination: string) {
this.destination = destination;
console.log('We're going to ${this.destination}!');
this.nonexistent = destination;
//Error: Property 'nonexistent' does not exist on type 'FieldTrip'.
}
}
- destination은 string으로 명시적선언이 되어있어서 FieldTrip 클래스 인스턴스에 할당되고 접근할 수 있다.
- 클래스가 nonexistent 속성을 선언하지 않았기 떄문에 생성자에서 this.nonexistent 할당은 허용되지 않음.
//2.
const trip = new FieldTrip('planetarium');
trip.destination;
trip.nonexistant;
//Error: Property 'nonexistant' does not exist on type 'FieldTrip'
- 클래스 속성을 명시적으로 선언하면 타입스크립트는 클래스 인스턴스에서 무엇이 허용되고 안되는지 빠르게 이해할 수 있따.
- 나중에 클래스 인스턴스가 사용될 때, 코드가 trip.nonexistant 처럼 클래스인스턴스에 존재하지 않는 멤버에 접근하려고 시도하면 타입스크립트는 타입오류를 발생.
함수속성
인스턴스
: 클래스의 복제본//1.
class WithMethod {
myMethod() {}
}
new WithMethod().myMethod === new WithMethod().myMethod;
- myFunction(){} 과 같이 멤버 이름 뒤에 괄호를 붙이는 메서드 접근방식을 보면,
- 메서드 접근방식은 함수를 클래스 프로토타입에 할당하므로 모든 클래스 인스턴스는 동일한 함수 정의를 사용한다.
- WithMethod 클래스는 모든 인스턴스가 참조할 수 있는 myMethod 메서드를 선언한다.
//2.
class WithProperty {
myProperty: () => {}
}
new WithMethod().myProperty === new WithMethod().myProperty; //false
- 값이 함수인 속성을 선언하는 방식도 있다.
- 이렇게 하면, 클래스의 인스턴스 당 새로운 함수가 생성되며, 항상 클래스 인스턴스를 가리켜야 하는 화살표함수에서 this 스코프를 사용하면 클래스 인스턴스 당 새로운 함수를 생성하는 시간과 메모리 비용측면에서 유용
- WithProperty 클래스는 이름이 myProperty인 단일 속성을 포함하며 각 클래스 인스턴스에 대해 다시 생성되는 () => void 타입이다.
//3.
class WithPropertyParameters {
takesParameters = (input: boolean) => input ? 'yes' : 'no';
}
const instance = new WithPropertyParameters();
instance.takesParameters(true);
instance.takesParameters(123);
//Error: Argument of type 'number' is not assignable to parameter of type 'boolean'
- 함수 속성에는 클래스 메서드와 독립함수의 동일한 구문을 사용해 매개변수와 반환 타입을 지정할 수 있다.
- 결국 함수속성은 클래스 멤버로 할당된 값이고, 그 값은 함수이다.
- WithPropertyParameters 클래스는 타입이 (input: boolean) => string 인 takesParameters 속성을 가진다.
초기화검사
//1.
class WithValue {
immediate = 0;
later: number; //constructor에서 할당
mayBeUndefined: number | undefined; //undefined가 되는 것이 허용됨
unused: number;
//Error: Property 'unused' has no initializer and is not definitely assigned in the constructor.
constructor() {
this.later = 1;
}
}
- WithValue 클래스는 unused 속성에 값을 할당하지 않았고, 타입스크립트는 이 속성을 타입오류로 인식.
//2.
class MissingInitializwr {
property: string;
}
new MissingInitializer().property.length;
// TypeError: Cannot read property 'length' of undefined
- 엄격한 초기화 검사가 없다면, 비록 타입 시스템이 undefined 값에 접근할 수 없다고해도 클래스 인스턴스는 undefined 값에 접근할 수 있다.
- 엄격한 초기화검사가 수행되지 않으면 올바르게 컴파일 되지만 결과 자바스크립트는 런타임 시 문제가 발생한다.
확실하게 할당 된 속성
class ActivitiesQueue {
pending!: string[];
initialize(pending: string[]) {
this.pending = pending;
}
next() {
return (
this.pending.pop();
)
}
}
const activities = new ActivitiesQueue();
activities.initializer(['eat', 'sleep', 'learn'])
activities.next();
- ActivitiesQueue 클래스는 생성자와는 별도로 여러번 다시 초기화될 수 있으므로 pending 속성은 !와 함께 할당되어야 한다.
- 클래스 속성에 대해 엄격한 초기화 검사를 비활성화하는 것은 종종 타입 검사에는 적합하지 않은 방식으로 코드가 설정된다는 신호이다.
- ! 어서션을 추가하고 속성에 대한 타입 안정성을 줄이는대신 클래스를 리팩터링해서 어서션이 더이상 필요하지 않도록하자.
선택적속성
class MissingInitializer {
property?: string;
}
new MissingInitializer().property?.length;
new MissingInitializer().property.length;
//Error: Objectt is possibly 'undefined'.
- MissingInitializer 클래스는 property를 옵션으로 정의했으므로 엄격한 속성초기화검사와 관계없이 클래스 생성자에서 할당하지않아도 된다.
읽기전용속성
//1.
class Quote {
readonly text: string;
constructor(text: string) {
this.text;
}
emphasize() {
this.text += '!';
//Error: Cannot assign to 'text' because it is a read-only property
}
}
const quote = new Quote(
'There is a brilliant child locked inside every student'
)
Quote.text = 'Ha!'
//Error: Cannot assign to 'text' because it is a read-only property.
- readonly로 선언된 속성은 선언된 위치 또는 생성자에서 초깃값만 할당할 수 있다.
- 클래스 내의 메서드를 포함한 다른 모든 위치에서 속성은 읽을 수만 있고, 쓸 수는 없다.
- Quote 클래스의 text 속성은 생성자에서는 값이 지정되지만 다른 곳에서 값을 지정하려하면 오류뜬다.
- npm 패키지로 게시한 코드를 사용하는 외부인이 readonly제한자를 존중하지 않을 수 있다. 특히, 자바스크립트를 작성 중이고 타입검사를 하지 않는 사용자라면 더욱 그러함.
- 진정한 읽기전용 보호가 필요하다면 # private 필드나 get() 함수속성 사용을 고려한다.
//2.
class RandomQuote {
readonly explicit: string = 'Home is the nicest word there is.';
readonly implicit = 'Home is the nicest word there is.'
constructor() {
if (Math.random () > 0.5) {
this.explicit = 'We start learning the minute we're born.';
this.implicit = 'We start learning the minute we're born.';
//Error: Type ''We start learning the minute we're born.'' is not assignable to type ''Home is the nicest word there is.''
}
}
}
const quote = new RandomQuote();
quote.explicit; //타입: string
quote.implicit; //타입: 'Home is the nicest word there is.'
- 원시타입의 초기값을 갖는 readonly로 선언된 속성은 다른 속성과 조금 다르다.
- 이런 속성은 더 넓은 원시값이 아니라 값의 타입이 가능한한 좁혀진 리터럴타입으로 유추됨.
- 타입스크립트는 값이 나중에 변경되지 않는다는 것을 알기 때문에 더 공격적인 초기타입 내로잉을 편하게 느낀다.
- const 변수가 let 변수보다 더 좁은 타입을 갖는 것과 유사함.
- 위 예제코드에서, 클래스 속성은 처음에는 모두 문자열리터럴로 선언되므로 둘 중 하나를 string 으로 확장하기 위해서는 타입애너테이션이 필요하다.
- 속성의 타입을 명시적으로 확장하는 작업이 자주 필요하진 않다.
- 그럼에도 불구하고 RandomQuote 에서 등장하는 생성자의 조건부 로직처럼 경우에 따라 유용할 수 있음
타입으로서의 클래스
//1.
class Teacher {
sayHello() {
console.log('Take chances, make mistakes, get messy!');
}
}
const teacher: Teacher;
teacher = new Teacher();
teacher = 'Wahoo!'
//Error: Type 'string' is not assignable to type 'Teacher'.
- Teacher클래스의 이름은 teacher 변수에 주석을 다는데 사용된다.
- teacher 변수에는 Teacher 클래스의 자체인스턴스처럼 Teacher클래스에 할당할 수 있는 값만 할당해야함.
//2.
class SchoolBus {
getAbilities() {
return (
['magic', 'shapeshifting']
)
}
}
const withSchoolBus = (bus: SchoolBus) => {
console.log(bus.getAbilities());
}
withSchoolBus(new SchoolBus());
withSchoolBus({
getAbilities: () => ['transmogrification']
});
withSchoolBus({
getAbilities: () => 123
//Error: Type 'number' is not assignable to type 'string[]'
})
- 타입스크립트는 클래스의 동일한 멤버를 모두 포함하는 모든 걕체타입을 클래스에 할당할 수 있는 것으로 간주한다.
- 타입스크립트의 구조적 타이핑이 선언되는 방식이 아니라 객체의 형태만 고려하기 때문
- withSchoolBus 는 SchoolBus 타입의 매개변수를 받는다.
- 매개변수로 SchoolBus 클래스인스턴스처럼 타입이 () => string[]인 getAbilities 속성을 가진 모든 객체를 할당할 수 있다.
- 대부분의 실제 코드에서 개발자는, 클래스 타입을 요청하는 위치에 객체의 값을 전달하지 않는다. 이러한 구조적인 확인 동작은 예상하지 못한것처럼 보일 수 있지만 자주 나타나지는 않음.
클래스와 인터페이스
//1.
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';
}
- Student 클래스는 name 속성과 study 메서드를 포함해 Learner 인터페이스를 올바르게 구현했지만,
- Slacker 에는 study가 누락 됐으므로 타입오류가 발생.
- 클래스에 의해 구현되는 인터페이스는 Leaner인터페이스에서 사용된 것처럼 인터페이스 멤버를 함수로 선언하기 위해 메서드 구문을 사용한다.
//2.
class Student implements Learner {
name;
//Error: Member 'name' implicitly has an 'any' type.
study(hours) {
//Error: Parameter 'hours' implicitly has an 'any' type.
}
}
- 인터페이스를 구현하는 것으로 클래스를 만들어도 클래스가 사용되는 방식은 변경되지 않는다.
- 클래스가 이미 인터페이스와 일치하는 경우 타입스크립트의 타입검사기는 인터페이스의 인스턴스가 필요한 곳에서 해당 인스턴스를 사용할 수 있도록 허용한다.
- 타입스크립트는 인터페이스에서 클래스의 메서드 또는 속성타입을 유추하지 않는다.
- Slacker 에제에서 study(hours){} 메서드를 추가했다면 타입스크립트는 타입애너테이션을 지정하지 않는한 hours 매개변수를 암시적으로 any로 간주함.
- 위의 예제는 다른형태로 구현한 Student 클래스는 멤버에 타입애너테이션을 제공하지 않기 때문에 암시적인 any 타입오류발생.
- 인터페이스를 구현하는 것은 순전히 안정성검사를 위해서이다.
- 모든 인터페이스 멤버를 클래스정의로 복사하지 않는다.
- 대신, 인터페이스를 구현하면 클래스 인스턴스가 사용되는 곳에서 나중에 타입검사기로 신호를 보내고 클래스 정의에서 표면적인 타입오류가 발생한다. 변수에 초기값이 있더라도 타입애너테이션을 추가하는 것과 용도가 비슷함.
다중인터페이스구현
//1.
interface Graded {
grades: number[];
}
interface Reporter {
report: () => string;
}
class ReportCard implements Graded, Reporter {
grades: number[];
constructor(grades: number[]) {
this.grades = grades;
}
report() {
return (
this.grades.join(',');
)
}
}
class Empty implements Graded, Reporter {}
// Error: Class 'Empty' incorrectly implements interface 'Graded'.
// Property 'grades' is missing in type 'Empty' but required in type 'Grade'.
// Error: Class 'Empty' incorrectly implements interface 'Reporter'.
// Property 'report' is missing in type 'Empty' but required in type 'Reporter'.
- 두 클래스에서 모두 Graded를 구현하려면 grades 속성을 가져야 하고, Reporter를 구현하려면 report 속성을 가져야 한다.
- Empty클래스에는 Graded와 Reporter 인터페이스를 제대로 구현하지 못했으므로 두가지 타입오류가 발생한다.
//2.
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() {
retrn (
"";
)
}
//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'.
}
- 실제로 클래스가 한번에 두 인터페이스를 구현할 수 없도록 정의하는 인터페이스가 있을 수 있다.
- 두개의 충돌하는 인터페이스를 구현하는 클래스를 선언하려고하면 클래스에 하나이상의 타입오류가 발생한다.
- AgeIsNumber와 AgeIsNotNumber 인터페이스는 age 속성을 서로 다른 타입으로 선언한다.
- AsNumber 클래스와 NotAsNumber 클래스 모두 두 인터페이스를 제대로 구현하지 못했음.
- `두 인터페이스가 매우 다른 객체형태를 표현하는 경우에는 동일한 클래스로 구현하지 않아야한다.`
클래스확장
- 타입스크립트는 다른 클래스를 확장하거나 하위 클래스를 만드는 자바스크립트 개념에 타입검사를 추가한다.
- 먼저 기본클래스에 선언된 모든 메서드나 속성은 파생클래스라고도하는 하위클래스에서 사용가능.
class Teacher {
teach() {
console.log('The surest test of discipline is its absence.')
}
}
class StudentTeacher extends Teacher {
learn() {
console.log('I cannot afford the luxury of a closed mind.')
}
}
const teacher = new StudentTeacher();
teacher.teach();
teacher.learn();
teacher.other();
// Error: Property 'other' does not exist on type 'StudentTeacher'.
- Teacher는 StudentTeacher 하위클래스의 인스턴스에서 사용할 수 있는 teach메서드를 선언한다.
할당가능성 확장
//1.
class Lesson {
subject: string;
constructor(subject: string) {
this.subject = subject
}
}
class OnlineLesson extends Lesson {
url: string;
constructor(subject: string, url: string){
super(subject);
this.url = url;
}
}
let lesson: Lesson
lesson = new Lesson("coding");
lesson = new OnlineLesson("coding", "oreilly.com");
let online: OnlineLesson
online = new OnlineLesson("coding", "oreilly.com")
online = new Lesson("coding")
// Error: Property 'url' is missing in type 'Lesson' but required in type 'OnlineLesson';
재정의 된 생성자
class GradeAnnouncer {
message: string;
constructor(grade: number) {
this.message = grade >= 65 ? "Maybe nest time ...": "You pass!"
}
}
class PassingAnnouncer extends GradeAnnouncer {
constructor() {
super(100)
}
}
class FailingAnnouncer extends GradeAnnouncer {
constructor() {}
//Error: Constructors for subclasses must contain a 'super' call.
}
- PassingAnnouncer의 생성자는 number 인수를 사용해 기본 클래스인 GradeAnnouncer의 생성자를 올바르게 호출하는 반면, FailingAnnouncer는 기본 생성자를 올바르게 호출하지 않아서 타입오류가 발생한다.
- 자바스크립트 규칙에 따르면 하위 클래스의 생성자는 this 또는 super에 접근하기 전에 반드시 기본 클래스의 생성자를 호출해야한다.
- 타입스크립트는 super()를 호출하기 전에 this 또는 super에 접근하려고 하는 경우 타입오류를 보고한다.
- ContinuedGradesTally 클래스는 super()를 호출하기 전에 생성자에서 this.grades 를 잘못 참조한다.
class GradesTally {
grades: number[] = [];
addGrades(...grades: number[]) {
this.grades.push(...grades)
}
}