class Dict {
private words: Words
constructor() {
this.words = {}
}
// 단어를 추가
add(word: Word) {
if (this.words[word.term] === undefined) {
this.words[word.term] = word.def
}
}
}
class Word {
constructor(
public term: string,
public def: string
) {}
}
const kimchi = new Word("kimchi", "한국의 음식")
class Dict
의 메소드 add
에서 단어를 추가하기 위해서는 term
과 def
가 public
이어야 한다.
kimchi.def = "xxx"
그러나 이 경우, 위처럼 인스턴스에서 수정이 가능하다.
class Word {
constructor(
public readonly term: string,
public readonly def: string
) {}
}
public
property의 직접적인 수정이 불가능하도록 하려면 readonly
속성을 부여하면 된다.
클래스를 통해 인스턴스를 생성할 필요 없이, 클래스의 속성 또는 메서드를 사용하고자 할 때 사용하는 키워드
class Dict {
private words: Words
constructor() {
this.words = {}
}
// 단어를 추가
add(word: Word) {
if (this.words[word.term] === undefined) {
this.words[word.term] = word.def
}
}
static hello() {
return "hello"
}
}
Dict.hello()
인스턴스를 생성하지 않고, 클래스에서 바로 호출이 가능하다.
static
은 JavaScript의 키워드이기 때문에 컴파일 후에도 남아있게 된다.
어떠한 객체나 클래스를 생성 했을 때 가져야 할 속성 또는 메서드를 정의할 수 있다.
interface
는abstract class
와는 달리 JavaScript와 같은 동적 타입 언어 환경에서는 다뤄지지 않기 때문에, 인터페이스를 사용하여 파일 크기를 줄일 수 있다.
type
과 interface
의 차이// 타입이 특정 값을 가지도록 제한
type Team = "red" | "blue" | "yellow"
// 타입 alias
type Health = number;
// 객체의 스펙 정의
type Player = {
nickname: string,
team: Team
health: Health // 타입 alias
}
const nico: Player = {
nickname: "nico",
team: "yellow",
health: 10
}
type
은 string, number 등의 concrete 타입 뿐만 아니라, 특정 값으로 제한할 수도 있다.
위 예시의 경우, Team
을 red
, blue
, yellow
라는 특정 string으로 타입을 제한하였다.
인터페이스는 오직 객체의 스펙을 정의하는 데에만 사용할 수 있다.
type Team = "red" | "blue" | "yellow"
type Health = 1 | 5 | 10
interface Player {
nickname: string,
team: Team
health: Health
}
❗️ interface
를 정의할 때에는 =
을 사용하지 않는다.
interface Hello = string // Err!
인터페이스는 객체의 스펙을 정의하는 목적으로만 사용 가능하다.
interface User {
name: string
}
interface Player extends User {
}
const nico: Player = {
name: "nico"
}
class를 상속하는 class가 있듯, 인터페이스 또한 extends
키워드를 사용해 인터페이스를 확장할 수 있다.
⇒ 문법 구조가 객체지향 프로그래밍과 유사하다!
interface
는 extends
키워드를 사용하여 확장이 가능한 반면, type
은 확장이 불가능하기 때문에 &
연산자를 사용해야 한다.
type과 interface의 가장 큰 차이점은 타입의 확장 가능 여부!
✨ 따라서, 객체의 스펙을 정의하는 용도로 사용할 때에는 가급적 확장이 가능한 인터페이스로 선언하는 것이 좋다.
interface
는 선언을 병합(축적)할 수 있으나, type
은 선언이 중복되면 에러가 발생한다.
interface User {
name: string
}
interface User {
lastName: string
}
interface User {
health: number
}
위처럼 같은 인터페이스를 반복 선언하여 다른 property를 축적할 수 있다.
type User {
name: string
}
type User { // Err: Duplicate identifier 'User'
lastName: string
}
type에서는 위처럼 에러가 발생한다.
abstract class
대신 interface
사용하기abstract class User {
constructor(
protected firstName: string,
protected lastName: string
)
abstract sayHi(name: string): string
abstract fullName(): string
}
abstract class
(추상 클래스)는 다른 클래스가 따라야 할 청사진을 제시한다.
또한 추상 클래스로는 인스턴스를 만들 수 없다.
class Player extends User {
fullName() {
return `${this.firstName} ${this.lastName}`
}
sayHi(name: string) {
return `Hello ${name}. My name is ${this.fullName()}`
}
}
User
를 상속받는 Player
에서는 sayHi
메소드와 fullName
메소드를 반드시 구현해야한다.
이렇게 만든 추상 클래스(abstract
)는 JavaScript로 컴파일 시 결국 일반 class로 컴파일된다.
그렇다면 추상 클래스는 왜 사용하는 걸까?
⇒ 다른 class들이 표준화된 형태(property, 메소드)를 갖도록 해주는 청사진을 만들기 위해 사용!
그러나! 우리는 Player
의 인스턴스를 만들어 사용할 뿐, 사실 상 JavaScript에서 User
는 사용되지 않기 때문에, JavaScript로 컴파일 될 필요없는 부분이다.
⇒ ✨ 이때가 바로 인터페이스를 사용해야 할 때!!
인터페이스는 컴파일시 JavaScript로 변환되지 않고 사라지기 때문에, 추상 클래스보다 가벼운 코드를 작성할 수 있다.
⇒ implements
키워드를 사용한다!
implements
키워드를 사용하여 확장할 시, 클래스가 인터페이스에 정의된 실행 규칙을 따라야 한다.
interface User {
firstName: string,
lastName: string,
sayHi(name: string): string
fullName(): string
}
class Player implements User {
// Err!
// Class 'Player' incorrectly implements interface 'User'.
// Type 'Player' is missing the following properties from type 'User': firstName, lastName, sayHi, fullName
}
인터페이스 User
의 property를 따르라는 에러가 발생하게 된다.
이처럼 implements
키워드를 사용하면 class가 인터페이스의 형태를 따르게 할 수 있을 뿐만 아니라, JavaScript로 컴파일되지 않기 때문에 코드가 더욱 가벼워진다.
type User = {
firstName: string,
lastName: string,
sayHi(name: string): string
fullName(): string
}
class Player implements User {
...
}
type
또한 implements
키워드를 사용하여 class가 type
을 상속받을 수 있다.
class Player implements User {
constructor(
private firstName: string, // Err !!
public lastName: string
){}
fullName() {
return `${this.firstName} ${this.lastName}`
}
sayHi(name: string) {
return `Hello ${name}. My name is ${this.fullName()}`
}
}
단, 인터페이스를 상속할 때에는 property를 private
, protected
로 만들 수 없다.
interface User {
firstName: string,
lastName: string,
sayHi(name: string): string
fullName(): string
}
interface Health {
health: number
}
class Player implements User, Health {
constructor(
public firstName: string,
public lastName: string,
public health: number
){}
fullName() {
return `${this.firstName} ${this.lastName}`
}
sayHi(name: string) {
return `Hello ${name}. My name is ${this.fullName()}`
}
}
위처럼 한 클래스에서 여러 개의 인터페이스를 동시에 상속하는 것도 가능하다.
function makeUser(user: User) {
return "h1"
}
makeUser({
firstName: "nico",
lastName: "las",
fullName: () => "xx",
sayHi: (name) => "string"
})
물론, 클래스와 마찬가지로 인터페이스도 타입으로 지정이 가능하다!
위 경우, 인자의 타입으로 인터페이스를 사용하여 객체의 형태를 지정해주었다.
function makeUser(user: User): User {
return {
firstName: "nico",
lastName: "las",
fullName: () => "xx",
sayHi: (name) => "string"
}
}
만약 인터페이스를 return한다면, class를 return하는 것처럼 new User {...}
와 같은 방식으로 작성할 필요없이 인터페이스의 형태에 맞는 객체를 반환해주면 된다.
⇒ new
는 class의 문법!
1️⃣ type
은
1) 객체의 형태 및 타입을 정의,
2) 타입 alias 생성,
3) 특정된 값의 타입 생성
용도로 사용할 수 있다.
2️⃣ interface
는 객체의 형태 및 타입을 정의하는 용도로 사용할 수 있다.
3️⃣ type
이 type
을 상속하기 위해서는 &
연산자를 사용하여야 한다.
또한, type
은 같은 이름의 타입을 선언하여 property를 병합할 수 없다.
4️⃣ interface
가 interface
를 상속하기 위해서는 extends
키워드를 사용하여야 한다. 또한, interface
은 같은 이름의 인터페이스를 선언하여 property를 병합할 수 있다.
✨ 3️⃣, 4️⃣번이 type
과 interface
의 가장 큰 차이점!
5️⃣ 인터페이스와 implements
키워드를 사용하여 class가 특정 메소드나 property를 상속하도록 할 수 있다.
6️⃣ 인터페이스는 추상 클래스와 비슷한 보호 장치의 역할을 하나, JS로 컴파일되지 않기 때문에 추상 클래스를 사용할 때보다 파일 크기를 줄일 수 있다.
7️⃣ type
과 interface
모두 class가 상속받을 수 있기 때문에 추상 클래스를 대체할 수 있다.
그러나 class나 객체의 형태를 정의하는 용도로 사용할 때에는 확장성을 고려하여 interface
를 사용하는 것이 좋다.
참고
타입스크립트 핸드북
타입스크립트 가이드북
타입스크립트 딥다이브