class Department {
name: string;
constructor(n: string) {
this.name = n;
}
describe() {
console.log('deparment : ' + this.name);
}
}
const accounting = new Department('Accounting');
class Department {
name: string;
constructor(n: string) {
this.name = 'hello';
}
describe(this: Department) {
console.log('deparment : ' + this.name);
}
}
const accoutningCopy = { describe: accounting.describe };
accoutningCopy.describe(); // => error
const accoutningCopy = { name: 'copy', describe: accounting.describe };
accoutningCopy.describe(); // => deparment : copy
class Department {
name: string;
employees: string[] = [];
constructor(n: string) {
this.name = 'helo';
}
describe(this: Department) {
console.log('deparment : ' + this.name);
}
addEmployee(employee: string) {
this.employees.push(employee);
}
printEmployeeInformation() {
console.log(this.employees.length);
console.log(this.employees);
}
}
아래와 같이 employees에 추가할 수 있지만, class를 사용하는 방법은 확실한 한가지로 정하고 다른 방법은 가능하지 않도록 하는 것이 좋다.
규모가 큰 팀에서 일하면 사람마다 다른 방법을 사용할 수 있기 때문에, 이를 방지해야 한다.
accounting.addEmployee('max');
accounting.employees[2] = 'Anna';
// 모두 가능하다. 하지만 지양해야 한다.
따라서 클래스 외부에서 employees에 접근하는 것을 허용해서는 안된다.
이를 해결하는 방법은 private을 이용하는 것이다.
private의 의미는 employees가 생성된 객체 (클래스) 내부에서만 접근할 수 있는 속성이 되었다는 것을 의미한다
public name: string;
// public은 default 값이다.
private employees: string[] = [];
accounting.employees[2] = 'Anna'; //=> error
console.log(accounting.employees) // error
// 더이상 이렇게 접근할 수 없다
console.log(accounting.name) // => 정상동작
employees 속성이 private이고 Department 클래스 내에서만 접근할 수 있다.
Department로 employees가 추가되도록 하려면 addEmployee 메소드를 사용해야만 한다.
하지만 private, public은 TypeScript에서 지원하는 기능이다. 때문에 타입스크립트는 컴파일 도중에 검사하므로 에러를 잡아낼 수 있만, 자바스크립트의 런타임 환경에서는 public으로 인식되어 그대로 정상적으로 동작된다.
class Department {
public name: string;
private id: string;
private employees: string[] = [];
constructor(n: string, id: string) {
this.name = n;
this.id = id;
}
...
}
constructor에 들어갈 초기화되는 값이 많아질 수록 작성해야하는 코드가 많아진다. 이를 해결할 수 있는 방법은 다음과 같다.
class Department {
private employees: string[] = [];
constructor(private id: string, public name: string) {
}
...
}
이렇게 작성하면 위에 작성했던 것과 동일하게 동작한다.
초기화 후에 변경되어서도 안되는 특정 필드의 경우 사용한다.
팀 동료와 코드를 공유해햐 할 수도 있고, 시간이 지나면서 코드의 의도를 기억해 내기가 어렵기 때문에, 코드는 명확하게 정의하는 것이 좋다.
class Department {
private employees: string[] = [];
constructor(private readonly id: string, public name: string) {
}
...
}
super를 작성한 다음 추가 arguments를 작성해야 한다.
class ITDepartment extends Department {
admins: string[];
constructor(id: string, admins: string[]) {
super(id, 'IT');
this.admins = admins;
}
}
private과 다른 점은 해당 클래스 뿐 아니라 클래스를 확장하는 모든 클래서에서도 사용 가능하다는 것이다.
class Department {
protected employees: string[] = [];
constructor(private readonly id: string, public name: string) {}
...
}
class AccountingDepartment extends Department {
constructor(id: string, private reports: string[]) {
super(id, 'Accounting');
}
addEmployee(name: string) {
if (name === 'max') return;
// employees가 protected이기 때문에 다른 클래스임에도 불구하고 접근이 가능하다.
this.employees.push(name);
}
...
}
employees가 protected이기 때문에 다른 클래스임에도 불구하고 접근이 가능하다.
만약 private으로 했다면 다른 클래스에서는 접근이 불가능하다.
getter에는 인수가 없고 setter는 인수 하나를 받는다.
접근자 프로퍼티의 값을 읽으려고 시도하면 getter가 호출되고 값을 쓰려고 시도하면 setter가 호출된다.
getter가 없는 접근자 프로퍼티를 읽으려고 시도하면 undefined가 호출된다.
let user = {
name: "John",
surname: "Smith",
get fullName() {
return `${this.name} ${this.surname}`;
}
};
alert(user.fullName); // John Smith
접근자 프로퍼티를 사용하면 함수처럼 호출 하지 않고, 일반 프로퍼티에서 값에 접근하는 것처럼 평범하게 user.fullName을 사용해 프로퍼티 값을 얻을 수 있다.
한편, 위 예시의 fullName은 getter 메서드만 가지고 있기 때문에 user.fullName=을 사용해 값을 할당하려고 하면 에러가 발생한다.
let user = {
get fullName() {
return `...`;
}
};
user.fullName = "Test"; // Error (프로퍼티에 getter 메서드만 있어서 에러가 발생합니다.)
let user = {
name: "John",
surname: "Smith",
get fullName() {
return `${this.name} ${this.surname}`;
}
set fullName(value) {
[this.name, this.surname] = value.split(" ");
}
};
// 주어진 값을 사용해 set fullName이 실행됩니다.
user.fullName = "Alice Special"
alert(user.fullName); // Alice Special
alert(user.name); // Alice
alert(user.surname); // Special
이렇게 getter와 setter 메서드를 구현하면 객체엔 fullName이라는 '가상’의 프로퍼티가 생긴다. 가상의 프로퍼티는 읽고 쓸 순 있지만 실제로는 존재하지 않는다.
prototype이 아닌 클래스 함수 자체에도 메서드를 설정할 수 있다.
정적 메서드는 인스턴스없이 클래스에서 바로 호출이 가능하고 이런 특성 때문에 유틸리티 함수를 만드는데 유용하게 사용된다.
static 프로퍼티, 메소드
...
static fiscalYear = '2020';
static createEmployee(name: string) {
return { name: name };
}
...
static 프로퍼티, 메소드 호출
const employee1 = Department.createEmployee('te');
console.log('employee1', employee1, Department.fiscalYear);
//=> employee1 {name: 'te'} 2020
abstract 클래스, 메소드는 일부 상위 클래스를 기반으로 하는 모든 하위 클래스가 일부 공통 메소드 또는 속성을 공유하도록 하려는 경우 사용한다.
abstract 키워드로 표기된 클래스들은 자체적으로 인스턴스화할 수 없다. 이는 기본적으로 상속되어야 할 클래스일 뿐이다.
이를 통해 상속되는 클래스가 인스턴스화되고 구체적인 구현을 제공하도록 할 수 있다.
abstract class Department {
protected employees: string[] = [];
constructor(protected readonly id: string, public name: string) {}
abstract describe(this: Department): void;
...
}
Department는 추상화되었기 때문에 인스턴스화할 수 없다.
class ITDepartment extends Department {
admins: string[];
constructor(id: string, admins: string[]) {
super(id, 'IT');
this.admins = admins;
}
describe() {
console.log('ITdepartment');
}
}
인터페이스는 일반적으로 타입 체크를 위해 사용되며 변수, 함수, 클래스에 사용할 수 있다. 인터페이스는 여러가지 타입을 갖는 프로퍼티로 이루어진 새로운 타입을 정의하는 것과 유사하다. 인터페이스에 선언된 프로퍼티 또는 메소드의 구현을 강제하여 일관성을 유지할 수 있도록 하는 것이다.
interface Person {
name: string;
age: number;
greet(phrase: string): void;
}
let user1: Person;
user1 = {
name: 'yeye',
age: 12,
greet(phrase: string) {
console.log(phrase + ' ' + this.name);
},
};
user1.greet('hi there');
사용자 정의 타입과 다른 점은 인터페이스의 경우 주로 객체의 구조를 정의할 때 사용한다. 인터페이스는 타입으로서 사용할 수 있다.
interface Greetable {
name: string;
greet(phrase: string): void;
}
let user2: Greetable; // <= 타입으로 사용되는 인터페이스
user2 = {
name: 'yeye',
// age: 12, // => error
greet(phrase: string) {
console.log(phrase + ' ' + this.name);
},
};
상속은 하나의 상위 클래스를 상속받을 수 있지만, 인터페이스는 쉼표로 구분하여 여러 개를 구현할 수 있다.
추상 클래스의 경우 추상적인 부분과 구체적인 구현 부분을 혼합하여 작성할 수 있지만, 인터페이스의 경우 구체적인 세부사항은 작성하지 않는다.
interface Greetable {
name: string;
greet(phrase: string): void;
}
interface Greetable2 {
name: string;
age: number;
greet(phrase: string): void;
}
class Person2 implements Greetable, Greetable2 {
name: string;
age = 30;
constructor(n: string) {
this.name = n;
}
greet(phrase: string) {
console.log('hi');
}
}
let user4 = new Person2('yaa');
console.log(user4);
인터페이스에서는 public, private은 사용할 수 없지만, readonly는 사용할 수 있다.
즉, 객체가 초기화되면 변경할 수 없도록 할 수 있다.
interface Greetable2 {
readonly name: string;
age: number;
greet(phrase: string): void;
}
user4.name = 'hehe' // => error (readonly이기 때문)
인터페이스 확장이 가능하다.
interface Named {
readonly name: string;
}
interface Greetable2 extends Named {
greet(phrase: string): void;
}
class Person3 implements Greetable {
name: string;
age = 30;
constructor(n: string) {
this.name = n;
}
greet(phrase: string) {
console.log('hi');
}
}
쉼표를 통해 여러개의 인터페이스에서 확장할 수 있다.
interface Greetable2 extends Named, Named2, Named3 {
greet(phrase: string): void;
}
인터페이스를 사용하여 함수의 타입을 지정할 수 있다.
interface AddFn {
(a: number, b: number): number;
}
let add: AddFn;
add = (n1: number, n2: number) => {
return n1 + n2;
};
속성 이름 다음에 물음표를 추가하여 선택적 속성을 지정할 수 있다.
interface Named {
readonly name?: string; //=> optional
outputName?: string; //=> optional
}
interface Greetable2 extends Named {
greet(phrase: string): void;
}
class Person3 implements Greetable2 {
name?: string; //=> optional
age = 30;
constructor(n?: string) {
if (n) {
this.name = n;
}
}
greet(phrase: string) {
if (this.name) {
console.log('hi' + this.name);
} else {
console.log('hi');
}
}
}
let person = new Person3();