
타입스크립트에서 인터페이스는 다양한 요소에 대한 약속을 정의하는 데 사용되고, 이를 통해 타입 체크를 수행한다. 주로 변수, 함수, 클래스 등 다양한 코드 요소에 적용될 수 있다.
인터페이스는 프로퍼티와 메소드를 가질 수 있다는 점에서 클래스와 유사하나 직접 인스턴스를 생성할 수 없고, 모든 메소드는 선언만 가능하며 구현은 인터페이스를 구현하는 클래스에서 제공되어야 한다. 단, 추상 클래스의 추상 메소드와 달리 abstract 키워드를 사용하지 않는다.
logAge 함수에서 받는 인자의 형태는 age를 속성으로 갖는 객체다.
인자를 받을 때 단순한 타입 뿐만 아니라 객체의 속성 타입까지 정의할 수 있다.
let person = { name: '덕수', age: 35 };
function logAge(obj: { age: number }) {
console.log(obj.age); // 35
}
logAge(person); // 28
여기에 인터페이스를 적용해보자!
logAge 함수의 인자는 personAge라는 타입을 가져야하며, 좀 더 명시적으로 바뀌었다.
더불어 다음과 같이 추론할 수 있다. 인터페이스를 인자로 받아 사용할 때, 인터페이스의 속성 개수와 인자로 받는 객체의 속성 개수를 일치시키지 않아도 된다. 또한 인터페이스에 선언된 속성 순서를 지키지 않아도 된다.
interface personAge {
age: number;
}
function logAge(obj: personAge) {
console.log(obj.age);
}
let person = { name: '덕수', age: 35 };
logAge(person); // 35
인터페이스는 변수의 타입으로 사용할 수 있다.
인터페이스를 타입으로 선언한 변수는 해당 인터페이스를 준수해야한다. 그래서 새로운 타입을 정의하는 것과 유사하다.
// 인터페이스 정의
interface Todo {
id: number;
content: string;
completed: boolean;
}
// 변수 todo의 타입으로 Todo 인터페이스 선언
let todo: Todo;
// 변수 todo는 Todo 인터페이스를 준수해야함
todo = { id: 1, content: 'typescript', completed: true };
인터페이스를 사용해 함수의 파라미터의 타입을 선언할 수 있다.
예시에서 Todo라는 인터페이스를 정의하고, 이 인터페이스를 함수의 파라미터 todo 의 타입으로 사용한다.
addTodo 함수는 Todo 인터페이스를 준수하는 파라미터 todo를 받아 배열에 추가한다. 이렇게 하면 함수를 호출할 때 Todo 인터페이스의 구조를 따르는 객체만 전달할 수 있다.
따라서 함수에 객체를 전달할 때 복잡한 매개변수 체크가 필요없어 유용하다.
// 인터페이스의 정의
interface Todo {
id: number;
content: string;
completed: boolean;
}
let todos: Todo[] = [];
// 파라미터 todo의 타입으로 Todo 인터페이스를 선언
function addTodo(todo: Todo) {
todos = [...todos, todo];
}
// 파라미터 todo는 Todo 인터페이스를 준수해야함
const newTodo: Todo = { id: 1, content: 'typescript', completed: false };
addTodo(newTodo);
console.log(todos)
// [ { id: 1, content: 'typescript', completed: false } ]
인터페이스는 함수의 타입으로 사용할 수 있다.
함수의 인터페이스에는 타입이 선언된 파라미터 리스트와 리턴 타입을 정의한다.
예시를 보면, SuareFunc 이라는 함수 타입의 인터페이스를 정의하고, 해당 인터페이스를 구현하는 함수 squarFunc 을 선언했다.
이 함수는 파라미터 num을 받고, number 타입을 반환하는 것이 해당 함수 인터페이스의 규칙이다.
// 인터페이스의 정의
interface SquareFunc {
(num: number): number;
}
// 함수 인테페이스를 구현하는 함수는 인터페이스를 준수해야함
const squareFunc: SquareFunc = function (num: number) {
return num * num;
}
console.log(squareFunc(10)); // 100
클래스 선언문의 implements 뒤에 인터페이스를 선언하면, 해당 클래스는 지정된 인터페이스를 반드시 구현해야 한다.
클래스가 특정 인터페이스를 구현한다면, 해당 클래스를 사용하는 코드에서는 인터페이스에서 정의한 메소드와 프로퍼티를 안전하게 활용할 수 있다.
인터페이스와 클래스의 연결은 코드의 일관성을 유지하고, 재사용성과 확장성을 높인다.
// 인터페이스 정의
interface MemoI {
id: number;
content: string;
completed: boolean;
}
// Memo 클래스는 MemoI 인터페이스를 구현해야함
class Memo implements MemoI {
constructor(public id: number, public content: string, public completed: boolean) {}
}
const memo = new Memo(1, 'typescript', false);
console.log(memo);
인터페이스는 프로퍼티 뿐만 아니라 메소드도 포함할 수 있다.
위에서 말했듯이, 인터페이스를 구현하는 클래스는 인터페이스에서 정의한 프로퍼티와 추상 메소드를 반드시 구현해야 한다.
// 인터페이스 정의
interface NoteI {
name: string;
sayHoo(): void;
}
// 인터페이스를 구현하는 클래스는 인터페이스에서 정의한 프로퍼티와 추상 메소드를 반드시 구현해야함
class Note implements NoteI {
// 인터페이스에서 정의한 프로퍼티의 구현
constructor(public name: string) {}
// 인터페이스에서 정의한 추상 메소드 구현
sayHoo() {
console.log(`안녕, ${this.name} 노트필기를 하자!`);
}
}
// 파라미터 note의 타입으로 NoteI 인터페이스를 선언
function greeter(note: NoteI): void {
note.sayHoo();
}
const no = new Note('덕배');
greeter(no); // 안녕, 덕배! 노트필기를 하자!
" 오리처럼 꽥꽥 소리를 내면 오리다 "
덕 타이핑은 위 말처럼 객체의 타입을 명시적으로 선언하지 않고도 객체의 사용 방식을 통해 타입을 결정하는 것이다.
인터페이스를 구현했다는 것만이 타입 체크를 통과하는 유일한 방법은 아니다. 객체의 실제 타입보다는 객체가 가져야 하는 메소드나 프로퍼티의 존재 여부에 중점을 둔다.
아래 예제를 보면, 인터페이스 DuckI는 quack 메소드를 정의하고 있다.
makeNoise 함수는 인터페이스 DuckI를 구현한 클래스의 인스턴스 duck을 인자로 전달받는다.
클래스 OneDuck은 인터페이스 DuckI를 구현했다. 클래스 TwoDuck은 인터페이스 DuckI를 구현하지 않았지만, quack 메소드를 갖는다.
makeNoise 함수에 인터페이스 DuckI를 명시적으로 구현하지 않은 클래스 TwoDuck의 인스턴스를 인자로 전달해도 에러 없이 처리된다.
왜? 타입스크립트는 해당 인터페이스에서 정의한 프로퍼티나 메소드를 가지고 있다면, 그 인터페이스를 구현한 것으로 인정하기 때문이다.
이것을 바로 덕 타이핑(duck typing) 또는 구조적 타이핑(structural typing)이라고 한다.
interface DuckI {
quack(): void;
}
class OneDuck implements DuckI {
quack() {
console.log('꽥꽥!');
}
}
class TwoDuck {
quack() {
console.log('꽤애액꽤애액');
}
}
function makeNoise(duck: DuckI): void {
duck.quack();
}
makeNoise(new OneDuck()); // 꽥꽥!
makeNoise(new TwoDuck()); // 꽤애액꽤애액
변수에 사용할 경우의 예제도 보자! 변수 me 는 인터페이스 DuckI와 일치하지는 않지만, DuckI의 name 프로퍼티를 가지고 있기에 인정된다.
참고로 이러한 기능은 개발 단계에서 도움을 주기 위해 제공되는 것이지 자바스크립트의 표준이 아니다. 따라서 아래의 예제의 타입스크립트 파일을 자바스크립트 파일로 트랜스파일링하면 인터페이스가 삭제된다.
interface DuckI {
name: string;
}
function sayHello(duck: DuckI): void {
console.log(`안녕! ${this.name}`);
}
const me = { name: '덕배', age: 30 };
sayHello(me); // 안녕! 덕배
? 를 붙이며 생략해도 에러가 발생하지 않는다. interface UserInfo {
username: string;
password: string;
age?: number;
address?: string;
}
const userInfo: UserInfo = {
username: '최강덕배',
password: '123456',
};
console.log(userInfo);
// { username: '최강덕배', password: '123456' }
interface Mother {
name: string;
age?: number;
}
interface Son extends Mother {
grade: number;
}
const son: Son = {
name: '덕배',
age: 30,
grade: 3,
};
interface Mother {
name: string;
age?: number;
}
interface Father {
phone: string;
}
interface Son extends Mother, Father {
grade: number;
}
const son: Son = {
name: '덕배',
age: 30,
grade: 3,
phone: '010-1234-1234',
};
class Person {
constructor(public name: string, public age: number) {}
}
interface Developer extends Person {
skills: string[];
}
const developer: Developer = {
name: 'Lee',
age: 20,
skills: ['HTML', 'CSS', 'JavaScript']
}
타입스크립트에서 클래스는 객체 지향 프로그래밍의 핵심 요소로 사용된다.
클래스는 데이터와 해당 데이터를 처리하는 메소드를 함께 묶어 놓는 구조를 제공한다.
ES6 클래스는 클래스 몸체에 메소드만 포함할 수 있고, 클래스 프로퍼티는 반드시 생성자 내부에서 선언하고 초기화한다.
아래 예제는 ES6에서 문제없이 실행되는 코드지만, 확장자를 ts로 바꿔 타입스크립트 파일로 변경한 후, 컴파일하면 에러가 발생한다.
// js파일
class Person {
constructor(name) {
// 클래스 프로퍼티의 선언과 초기화
this.name = name;
}
sayHoo() {
console.log(`${this.name} 헬로우`);
}
}
// ts파일
class Person {
// 클래스 프로퍼티 사전 선언
name: string;
constructor(name: string) {
// 클래스 프로퍼티에 값을 할당
this.name = name;
}
sayHoo() {
console.log(`${this.name} 헬로우`);
}
}
const person = new Person('덕배');
person.sayHoo(); // 덕배 헬로우
타입스크립트 클래스는 클래스 기반 객체 지향 언어가 지원하는 private, public, protected 등의 접근 제어자를 사용하여 멤버 변수의 접근 권한을 제어할 수 있다.
타입스크립트의 경우, 접근 제어자를 생략한 클래스 프로퍼티와 메소드는 암묵적으로 public이 선언된다. 따라서 public으로 지정하고자 하는 멤버 변수와 메소드는 접근 제한자를 생략한다.
private는 해당 클래스 내부에서만 접근 가능하다.
public은 기본값이며 어디서나 접근 가능하다.
protected는 해당 클래스와 해당 클래스를 상속한 자식 클래스에서 접근 가능하다.
class Car {
public speed: number;
protected size: string;
private color: string;
constructor(speed: number, size: string, color: string) {
this.speed = speed;
this.size = size;
this.color = color;
}
accelerate() {
this.speed += 10;
}
getSpeed() {
return `${this.speed}스피드로 ${this.color} 차를 타고 달려보자!`;
}
}
const car = new Car(50, '대형차', 'black');
car.accelerate();
console.log(car.getSpeed()); // 60
// public은 클래스 인스턴스를 통해 클래스 외부에서 참조 가능
console.log(car.speed);
// protected는 클래스 인스턴스를 통해 클래스 외부에서 참조 불가능
// console.log(car.size); // error
// private는 클래스 인스턴스를 통해 클래스 외부에서 참조 불가능
// console.log(car.color); // error
class Bike extends Car {
constructor(speed: number, size: string, color: string) {
super(speed, size, color);
// public은 자식 클래스 내부에서 참조 가능
console.log(this.speed);
// protected는 자식 클래스 내부에서 참조 가능
console.log(this.size);
// private는 자식 클래스 내부에서 참조 불가능
// console.log(this.color); // error
}
}
접근 제어자는 생성자 파라미터에도 선언할 수 있다.
접근 제어자가 사용된 생성자 파라미터는 암묵적으로 클래스 프로퍼티로 선언되고, 생성자 내부에서 별도의 초기화가 없어도 암묵적으로 초기화가 수행된다.
class Car {
// 접근 제어자가 선언된 생성자 파라미터 speed는 클래스 프로퍼티로 선언되고 자동으로 초기화됨
// public이 선언되었으니, speed는 클래스 외부에서도 참조 가능
constructor(public speed: number) {}
}
const car = new Car(50);
console.log(car); // Car { speed: 50 }
console.log(car.speed); // 50
class Bike {
// 접근 제어자가 선언된 생성자 파라미터 speed는 멤버 변수로 선언되고 자동으로 초기화됨
// private가 선언되었으니, speed는 클래스 내부에서만 참조 가능
constructor(private speed: number) {}
}
const bike = new Bike(50);
console.log(bike); // Bike { speed: 50 }
// private로 선언된 bike.speed는 클래스 내부에서만 참조 가능
console.log(bike.speed);
// Property 'speed' is private and only accessible within class 'Bike'
class Car {
// speed는 생성자 내부에서만 유효한 지역 변수다.
constructor(speed: number) {
console.log(speed);
}
}
const car = new Car(50);
console.log(car); // Car { }
타입스크립트는 readonly 키워드를 사용할 수 있고, readonly가 선언된 클래스 프로퍼티는 선언 시 또는 생성자 내부에서만 값을 할당할 수 있다.
그 외의 경우에는 값을 할당할 수도, 변경할 수도 없고 오직 읽기만 가능한 상태가 된다.
class Person {
readonly name: string;
constructor(name: string) {
this.name = name; // 생성자에는 값을 할당할 수 있음
}
changeName(newName: string): void {
// 오류: readonly 프로퍼티는 다른 메소드에서 값을 변경할 수 없음
console.log('이름 변경 불가! 현재 이름은 ${this.name}')
}
}
const person = new Person('덕배');
console.log(person.name); // 읽기는 가능
// person.name = '덕수';
// 오류: readonly 프로퍼티는 외부에서 값을 변경할 수 없음
ES6 클래스에서 static 키워드는 클래스의 정적(static) 메소드를 정의한다.
정적 메소드는 클래스의 인스턴스가 아닌 클래스 이름으로 호출한다. 따라서 클래스의 인스턴스를 생성하지 않아도 호출할 수 있다.
// js파일
class Person {
constructor(name) {
this.name = name;
}
static staticMethod() {
// 정적 메소드는 this를 사용 불가
// 정적 메소드 내부에서 this는 클래스의 인스턴스가 아닌 클래스 자신을 가리킴
return 'staticMethod';
}
prototypeMethod() {
return this.name;
}
}
// 정적 메소드는 클래스 이름으로 호출함
console.log(Person.staticMethod()); // staticMethod
const person = new Person('덕배');
console.log(person.prototypeMethod()); // 덕배
// 정적 메소드는 인스턴스로 호출할 수 없음
// console.log(person.staticMethod());
타입스크립트에서는 static 키워드를 클래스 프로퍼티에도 사용할 수 있다.
정적 메소드와 마찬가지로 정적 클래스 프로퍼티는 인스턴스가 아닌 클래스 자체에 속하며, 클래스 이름으로 호출하고, 클래스의 인스턴스를 생성하지 않아도 된다.
// ts파일
class Person1 {
static counter = 0;
constructor() {
Person1.counter++;
}
}
const person1 = new Person1();
const person2 = new Person1();
console.log(Person1.counter); // 2
// 정적 프로퍼티 및 메소드는 클래스의 인스턴스가 아니라,
// 클래스 자체에 속해 있기 때문에 인스턴스를 통해 직접 접근할 수 없음
// console.log(person2.counter); // error
추상 클래스는 하나 이상의 추상 메소드를 가질 수 있고, 추상 메소드는 내용이 없이 메소드 이름과 타입만이 선언된 메소드를 말하며 선언할 때 abstract 키워드를 사용한다.
추상 클래스는 일반 메소드도 포함할 수 있고, 이 일반 메소드는 구현을 가지며 하위 클래스에서 필요에 따라 오버라이딩될 수 있다.
추상 클래스는 직접 인스턴스를 생성할 수 없고, 상속만을 위해 사용된다. 추상 클래스를 상속한 클래스는 추상 클래스의 추상 메소드를 반드시 구현하여야 한다.
abstract class Animal {
// 추상 메소드
abstract makeSound(): void;
// 일반 메소드
move(): void {
console.log('동물은 움직인다.')
}
}
// 직접 인스턴스 생성 불가
// new Animal();
class Cat extends Animal {
// 추상 클래스를 상속한 클래스는 추상 클래스의 추상 메소드를 반드시 구현해야함
makeSound() {
console.log('야옹~');
}
}
const myCat = new Cat();
myCat.makeSound(); // 야옹~
myCat.move(); // 동물은 움직인다.