class Animal {
constructor(name) {
this.name = name;
}
}
자바스크립트에서는 클래스 인스턴스 변수를 굳이 선언하지 않아도 생성자 메서드(contructor)에서 this 키워드로 자동 생성시킬 수 있다.
class Animal {
name: string; // 인스턴스 변수 선언
constructor(name: string) {
this.name = name;
}
}
하지만 타입스크립트에서는 생성자 메서드(constructor)와 함께 클래스 바디에 인스턴스 변수를 정의해 주어야 한다.
타입스크립트 인터페이스를 extends로 상속했듯이, 클래스도 부모/자식 관계를 맺으면서 상속이 가능하다.
유의할 점은, 자식이 상속할 수 있는 클래스의 갯수는 무조건 1개라는 점!
여러개의 클래스는 상속할 수 없다.
반대로, 부모 클래스는 여러 개의 자식 클래스를 가질 수 있다.
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
move(distanceInMeters: number = 0) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
class Snake extends Animal {
leg: number = 0;
// @Override Animal
constructor(name: string) {
super(name);
}
// @Override Animal
move(distanceInMeters = 5) { // 오버라이드 되서 굳이 또 매개변수 타입을 선언 할 필요 없다.
super.move(distanceInMeters);
}
poison() {
console.log('shoot poison !!');
}
}
class Horse extends Animal {
leg: number = 4;
// @Override Animal
constructor(name: string) {
super(name);
}
run() {
console.log('start Run !!');
}
}
위 코드에서는 extends 키워드를 사용해 Animal의 부모 클래스를 자식 클래스 둘(Horse와 Snake)이 상속했다.
상속된 클래스의 생성자 함수에 부모 클래스의 생성자를 실행 할 super()
를 호출하지 않으면 에러가 발생한다. (자식 클래스에 생성자 함수가 없으면 부모 클래스의 생성자 함수가 자동 호출되므로 super() 작성하지 않아도 된다.)
또한 자식 클래스는 부모 클래스에 정의되어있는 move() 라는 메서드를 오버라이드 했는데, 타입스크립트의 오버로딩이 비슷한 형태의 메서드를 유연하게 여러개 생성하는 것이라면, 오버라이드는 완전히 덮어씌워 버려 기존 함수를 개조하는 개념으로 이해하면 된다.
let sam: Snake = new Snake('Sammy the Python'); // 자식 클래스 생성자로 객체 생성
sam.leg; // 자식 클래스의 인스턴스 변수 : 0
sam.move(); // 오버라이드 한 부모클래스의 메소드 : 'Sammy the Python moved 5m'
sam.poison(); // 자식 클래스의 메소드 : 'shoot poison !!'
let tom2: Horse = new Horse('Tommy the Palomino');
tom2.leg; // 자식 클래스의 인스턴스 변수 : 4
tom2.move(34); // 오버라이드 한 부모클래스의 메소드 : 'Tommy the Palomino moved 34m'
tom2.run(); // 자식 클래스의 메소드 : 'start Run !!'
let tom: Animal = new Horse('Tommy the Palomino');
tom.leg; // 'Animal'형식에 'leg' 속성이 없습니다.
tom.move(34); // Tommy the Palomino moved 34m
tom.run();A // 'Animal'형식에 'run' 속성이 없습니다.
위와 같은 오류가 발생하는 이유는 부모클래스로 타입 형을 선언하고 자식 클래스를 생성해서 할당했기 때문이다.
위의 클래스를 타입스크립트 interface로 바꿔서 봐보자!
interface Animal {
name: string;
move(distanceInMeters: number): void;
}
interface Snake extends Animal {
// ...
}
interface Horse extends Animal {
leg: number;
run(): void;
}
let tom: Animal = {
name: '',
move: () => {},
};
tom.leg; // Error !! - 위의 대입한 객체에서 해당 데이터가 없다
tom.move(34); // Tommy the Palomino moved 34m
tom.run(); // Error !! - 위의 대입한 객체에서 해당 데이터가 없다
let tom2: Horse = {
leg: 4,
name: '',
move: () => {},
run: () => {},
};
tom2.leg;
tom2.move(34); // Tommy the Palomino moved 34m
tom2.run();
접근 제어자(Access Modifiers)는 클래스, 메서드 및 기타 멤버의 접근 가능성을 설정하는 객체 지향 언어의 키워드이다.
수식어 | 기능 | 적용 범위 |
---|---|---|
public | 어디서나 자유롭게 접근 가능 (기본값, 생략 가능) | 속성, 메서드 |
protected | 내 클래스를 상속한 자식 클래스 내에서까지만 접근 가능 | 속성, 메서드 |
private | 내 클래스에서만 접근 가능 | 속성, 메서드 |
모든 곳에 공개되어있어, 어디에서나 클래스 멤버에게 접근 가능하다는 것을 알려주는 키워드
타입스크립트에서는 public이 기본값이기 때문에 생략해도 무관하다!
class Animal {
public name: string;
public constructor(theName: string) {
this.name = theName;
}
public move(distanceInMeters: number) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
// 둘이 완전히 같은 구조이다.
class Animal {
name: string;
constructor(theName: string) {
this.name = theName;
}
move(distanceInMeters: number) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
단어 그대로 보호된 멤버로서, 외부에서 접근이 불가능하고 오로지 내 클래스와 상속한 자식 클래스에서만 접근이 가능한 키워드
class Animal {
// protected 수식어 사용
protected name: string;
constructor(name: string) {
this.name = name;
}
}
class Cat extends Animal {
getName(): string {
return `Cat name is ${this.name}.`;
}
}
let cat = new Cat('Lucy');
console.log(cat.getName()); // Cat name is Lucy.
console.log(cat.name); // Error - TS2445: Property 'name' is protected and only accessible within class 'Animal' and its subclasses.
cat.name = 'Tiger'; // Error - TS2445: Property 'name' is protected and only accessible within class 'Animal' and its subclasses.
console.log(cat.getName());
위 코드에서 Animal 클래스의 name 속성은 protected로 선언 되었기 때문에, 상속된 자식 클래스(Cat)에서 this.name으로 참조할 수는 있지만, 인스턴스에서 cat.name으로는 접근이 불가능하다.
단어 그대로 사적인 멤버이기 때문에 오로지 멤버를 선언한 클래스 내에서만 접근이 가능한 키워드
class Animal {
// private 수식어 사용
private name: string;
constructor(name: string) {
this.name = name;
}
}
class Cat extends Animal {
getName(): string {
return `Cat name is ${this.name}.`; // Error - TS2341: Property 'name' is private and only accessible within class 'Animal'
}
}
let cat = new Cat('Lucy');
console.log(cat.getName());
console.log(cat.name); // Error - TS2341: Property 'name' is private and only accessible within class 'Animal'.
cat.name = 'Tiger'; // Error - TS2341: Property 'name' is private and only accessible within class 'Animal'.
console.log(cat.getName());
위 코드를 보면 Animal 클래스의 name 속성은 private이기 때문에 파생된 자식 클래스(Cat)에서 this.name으로 참조가 불가능하고, 인스턴스에서도 cat.name으로의 접근도 불가능하다.
속성 | 설명 | 적용 범위 |
---|---|---|
static | 어디서나 자유롭게 접근 가능 (기본값, 생략 가능) | 속성, 메서드 |
readonly | 내 클래스를 상속한 자식 클래스 내에서까지만 접근 가능 | 속성, 메서드 |
정적 속성은 클래스 바디에서 속성의 타입 선언과 같이 사용하며, 정적 메서드와 다르게 클래스 바디에서 값을 초기화 할 수 없기 때문에 contructor 혹은 직접 초기화를 해야 한다.
class Cat {
// 정적 프로퍼티
static tail: number = 1; // 직접 초기화 하거나
static legs: number;
constructor() {
Cat.legs = 4; // Init static 프로퍼티.
}
static setLegs(n: number): void {
Cat.legs = n; // set static 프로퍼티.
}
}
console.log(Cat.tail); // 1
console.log(Cat.legs); // undefined
new Cat();
console.log(Cat.legs); // 4
Cat.setLegs(10)
console.log(Cat.legs); // 104
Readonly
를 사용하면 해당 속성은 읽기 전용이 된다.
메서드에는 적용 불가능
class Animal {
readonly name: string; // 읽기 전용 속성
constructor(n: string) {
this.name = n; // 읽기 전용은 초기화 할때만 값 대입 가능
}
}
let dog = new Animal('Charlie');
console.log(dog.name); // Charlie
// 그러나 초기화가 아닌 추후에 접근해서 할당은 불가능
dog.name = 'Tiger'; // Error - TS2540: Cannot assign to 'name' because it is a read-only property.
생성자 메서드에서 인수 타입 선언과 동시에 접근 제어자 / readonly를 사용하면 바로 속성 멤버로 정의할 수 있다.
이렇게 하면 멤버 변수를 굳이 선언하지 않아 코드가 간결해진다.
class Cat {
constructor(name: string, age: number) {} // ERROR !!!!!!!!!!
getName() {
return this.name; // ERROR !!!!!!!!!!
}
getAge() {
return this.age; // ERROR !!!!!!!!!!
}
}
// -------------------------------------------------------------------------
// 생성자 함수 인수에 public, readonly 키워드 사용
class Cat2 {
constructor(public name: string, readonly age: number) {}
getName() {
return this.name;
}
getAge() {
return this.age;
}
}
const cat = new Cat2('Neo', 2);
console.log(cat.getName()); // Neo
console.log(cat.getAge()); // 2
함수 매개변수에 클래스를 전달해서, 함수를 호출하면 클래스 초기화 되는 로직을 구현하고 싶다면 인터페이스의 구성 시그니처를 사용해야 한다.
interface IName {
new (PARAMETER: PARAM_TYPE): RETURN_TYPE // Construct signature
}
구성 시그니처는 인터페이스의 함수 타입 정의인 호출 시그니처와 비슷하지만, 앞에 new 키워드
를 사용한다는 점이 다르다.
즉, new 클래스() 초기화 생성자 함수 구조 타입을 정의한 것이라고 보면 된다.
이런 new 생성자 함수 자체 타입을 정의하는 이유는, 함수에 클래스 자체를 인자로 넘길 때 인식시키기 위해서이다. (클래스 자체를 타입으로 넘기면 에러)
interface IFullName {
firstName: string;
lastName: string;
}
interface IFullNameConstructor {
new (firstName: string): IFullName; // Construct signature
}
class Anderson implements IFullName {
public lastName: string;
constructor(public firstName: string) { // public firstName 접근 제어자로 선언해서 자동으로 this.firstName으로 만들어줌
this.lastName = 'Anderson';
}
}
// 클래스를 받아 대신 new 클래스 초기화 해주는 메소드
function makeSon(c: IFullNameConstructor, firstName: string) {
return new c(firstName);
}
function getFullName(son: IFullName) {
return `${son.firstName} ${son.lastName}`;
}
const tomas = makeSon(Anderson, 'Tomas');
const jack = makeSon(Anderson, 'Jack');
getFullName(tomas); // Tomas Anderson
getFullName(jack); // Jack Anderson
✅ 오버로딩 : 기존에 없는 새로운 메서드를 추가하는 것
✅ 오버라이딩 : 상속받은 메서드를 재정의 하는 것
구분 | Overriding | Overloading |
---|---|---|
접근 제어자 | 부모 클래스의 메서드의 접근 제어자보다 더 넓은 범위의 접근 제어자를 자식 클래스의 메서드에서 설정할 수 있다. | 모든 접근 제어자를 사용할 수 있다. |
리턴형 | 동일해야 한다. | 달라도 된다. |
메서드명 | 동일해야 한다. | 동일해야 한다. |
매개변수 갯수 / 타입 | 동일해야 한다. | 달라야만 한다. |
적용 범위 | 상속관계에서 적용된다. | 같은 클래스 내에서 적용된다. |
조상 클래스로부터 상속받은 메서드의 내용을 변경(재정의)하여 사용하는 것이다.
상속받은 메서드를 그대로 사용할 수도 있지만, 필요에 따라 메서드를 재정의하여 사용해야 하는 경우가 있기 때문!
💠 매개변수 숫자가 같아야 한다. 단, 매개변수명은 달라도 된다.
💠 매개변수 타입이 같거나, 하위 타입이어야 한다.
💠 리턴 타입이 같아야 한다.
/* 부모 class */
class Gun {
// 원본 함수
constructor(public name: string) {}
// 원본 함수
shot(times: number) {
for (let i = 0; i < times; i++) {
console.log('빵야!!!');
}
}
}
/* 부모를 상속한 자식 class */
class RailGun extends Gun {
constructor(public name: string) {
super(name);
}
shot(times2: number) {
for (let i = 0; i < times2; i++) {
console.log('탕탕탕');
}
}
}
let railgun = new RailGun('플라즈마', 99);
railgun.shot(3);
만약 오버라이딩한 부모의 메서드를 직접 불러오고 싶을 경우, super
키워드로 불러올 수 있다.
shot(times2: number) {
super.shot(times2); // 부모 메서드 호출
for (let i = 0; i < times2; i++) {
console.log('삐융~!!!');
}
}
오버로딩이란 하나의 클래스 안에서 같은 이름의 메서드를 여러개 정의하는 것을 말한다.
일반적으로 하나의 클래스 안에 같은 이름의 메서드를 정의하게 되면 에러가 발생하게 되지만, 다양한 기능을 위해 오버로딩의 조건을 만족하면 같은 이름의 메서드를 여러개 정의할 수 있다.
💠 매개변수의 개수나 타입이 달라야 한다.
💠 만일 매개변수를 늘려서 오버로딩 하고 싶다면, 추가된 매개변수에 옵셔널 프로퍼티(?:)를 사용한다.
class User {
public name: string = '';
public age: number = 0;
// @Overloading
join(name: string, age: number): User;
join(name: string, age: string, bool: boolean): string;
join(name: any, age: any, bool?: any): any {
if (bool) console.log(`bool is ${bool}`);
if (typeof age === 'number') {
return { name, age };
} else {
return '나이는 숫자로 입력해 주세요';
}
}
}
class BbyoRong extends User {
// ...
}
/* 오버로딩한 함수 호출 */
const user: BbyoRong = new BbyoRong();
console.log(user.join('김서영', 28));
// { name: '김서영', age: 28 }
console.log(user.join('김서영', '28', true));
// bool is true
// 나이는 숫자로 입력해 주세요
추상 클래스는 다른 클래스가 파생될 수 있는 기본 클래스로, 인터페이스와 유사하다.
인스턴스화 될 수 없는 클래스로, 자식 클래스에서 반드시 구현해야 하는 추상 메서드 및 속성을 정의할 수 있다.
인터페이스는 클래스나 변수가 정의할 타입 기본 뼈대를 정의하는 것이다.
추상클래스는 클래스 전용으로 클래스가 정의할 타입 뼈대를 정의하는 것이라고 이해하면 된다!
이외에도 인터페이스와의 차이점은 타입 뼈대 뿐만 아니라 아예 일반 클래스처럼 멤버를 정의할 수 있다는 점이 있다.
추상 클래스로 만들기 위해서는 abstract 키워드
를 사용해야 하는데, 클래스 뿐만 아니라 속성과 메서드에도 적용할 수 있다. (그러면 추상 속성, 추상 메서드가 된다.)
추상 클래스는 인터페이스와 같이 직접 인스턴스를 생성할 수 없기 때문에 인터페이스를 implements 한 것처럼 자식 클래스에 상속 extends를 하면 된다.
객체 지향 프로그래밍에서 구조와 공통 기능을 정의하면서, 구체적인 구현은 자식 클래스에게 위임하는 것이다.
추상 클래스는 클래스 상속 계층에서 공통된 속성이나 메서드를 공유할 수 있게 하며, 자식 클래스가 이를 바탕으로 특정 기능을 구현하도록 강제한다.
// 추상클래스를 상속만 하면 메소드 강제 구현 규칙으로 규격화만 하면 확장에 제한 없다 (opened)
// 추상화
abstract class Animal {
abstract void speak();
}
class Sheep extends Animal {
void speak() {
System.out.println("매에에");
}
}
class Lion extends Animal {
void speak() {
System.out.println("어흥");
}
}
// 기능 확장으로 인한 클래스가 추가되어도, 더이상 수정할 필요가 없어진다 (closed)
class HelloAnimal {
void hello(Animal animal) {
animal.speak();
}
}
public class Main {
public static void main(String[] args) {
HelloAnimal hello = new HelloAnimal();
Animal cat = new Cat();
Animal dog = new Dog();
Animal sheep = new Sheep();
Animal lion = new Lion();
hello.hello(cat); // 냐옹
hello.hello(dog); // 멍멍
hello.hello(sheep); // 매에에
hello.hello(lion); // 어흥
}
}
추상 클래스는 공통적으로 사용되는 속성이나 메서드를 정의해, 여러 자식 클래스가 일관된 인터페이스를 제공하도록 한다.
예를 들면 위 코드에서
abstract class Animal {
abstract void speak();
}
이 Animal 추상 클래스는 Animal 타입으로 다루는 모든 객체가 speak() 메서드
를 구현하도록 강제해, 모든 객체가 speak()
을 호출할 수 있게 보장된다.
추상 클래스는 자식 클래스가 특정 메서드를 반드시 구현하도록 강제한다.
자식 클래스가 추상 메서드를 구현하지 않으면, 에러가 발생한다.
class Dog extends Animal {
void speak() {
System.out.println("멍멍");
}
}
추상 클래스는 완전히 추상적인 인터페이스와는 다르게, 일부 메서드를 구현할 수 있다.
자식 클래스는 이 구현을 그대로 사용할 수 있고, 필요에 따라 재정의(override)할 수 있다.
이렇게 하면 중복 코드 작성이 줄어들고, 공통 기능을 상속받아 재사용할 수 있다.
abstract class Animal {
abstract void speak();
// 공통 기능 구현
void eat() {
System.out.println("먹고 있다.");
}
}
추상 클래스는 자체적으로 객체를 생성할 수 없다.
추상 클래스는 구체적으로 정의된 클래스(즉, 자식 클래스)를 통해서만 객체를 생성할 수 있다.
이를 통해 개발자는 상위 클래스에서 정의된 추상적인 개념이 아닌, 구체적인 구현체를 사용하게 된다.
// Animal animal = new Animal(); // 오류: 추상 클래스는 인스턴스화 불가
Animal dog = new Dog(); // 가능
/* ** Interface ** */
interface Animal {
name: string;
getName(): string;
}
class Dog implements Animal {
constructor(public name: string) {}
getName() {
return this.name;
}
}
const dog: Dog = new Dog('buldog');
/* ** 추상 클래스 ** */
abstract class Animal {
abstract name: string; // 추상 속성. 상속된 클래스에서 구현
abstract getName(): string; // 추상 메소드. 상속된 클래스에서 구현
// 추상 클래스는 인터페이스와 달리 이렇게 직접 멤버를 선언할수 도 있다.
// 이 멤버들은 상속 클래스에서 구현해도 되고 구현안해도 된다.
protected constructor(public legs: number) {}
getLegs() {
return this.legs;
}
getHello() {
return 'Hello World';
}
}
class Cat extends Animal {
constructor(public name: string, legs: number) {
super(legs);
}
getName() {
return this.name;
}
}
//! const animal: Animal = new Animal(); // Error - 추상 클래스는 직접 초기화 불가능. 무조건 상속을 해야함
const cat = new Cat('Luri', 4);
console.log(cat.getName()); // Luri
console.log(cat.legs); // 4
이렇게 보면, 추상클래스는 일반 멤버를 명시할 수 있다는 강점을 가지고 있다. (인터페이스는 이게 불가능)
abstract 클래스 Animal을 보면 abstract 키워드가 들어가지 않은 메서드 constructor(), getLegs(), getHello()를 볼 수 있는데, 추상클래스를 상속한 Cat 클래스에도 따로 이들을 오버로딩 하거나 오버라이딩 하지 않은 것을 볼 수 있다.
// 추상클래스를 상속한 Cat 쿨래스에서는 직접 명시 안해도, 추상클래스의 메소드를 이렇게 바로 사용도 할 수 있다.
console.log(cat.getLegs()) // 4
console.log(cat.getHello()) // 'Hello World'
하지만 상속받은 클래스에서 위와 같은 방식으로 추상클래스의 멤버에 바로 접근해서 직접적인 사용을 할 수 있다.
추상 클래스에도 new 생성자 함수 자체 타입이 존재한다.
type AbstractConstructor = abstract new (...args: any[]) => any;
type AbstractConstructor = abstract new (...args: any[]) => any;
// 추상 클래스
abstract class Animal {
abstract walk(): void;
breath() {
console.log('breath');
}
}
// 매개변수 Ctor은 constructor 약자
function animatableAnimal(Ctor: AbstractConstructor) {
abstract class StopWalking extends Ctor {
animate() {
console.log('animate');
}
}
return StopWalking;
}
class Dog extends animatableAnimal(Animal) { // 추상클래스 만드는 함수를 extends
walk() {
console.log('walk');
}
}
const dog = new Dog();
dog.breath(); // 'breath'
dog.walk(); // 'walk'
dog.animate(); // 'animate'