이전 글 - TypeScript: 클래스(Class)
클래스와 인터페이스
지금껏 JavaScript만을 다뤄본 개발자라면 인터페이스라는 개념은 익숙치 않을 것이다. 하지만 Java나 C# 등의 정적 타입 언어에서는 이미 많이 쓰이는 개념이다. 인터페이스는 클래스에서 구현부가 빠졌다고 이해하면 편하다. 즉, 어떠한 객체가 이러이러한 프로퍼티 혹은 메소드를 가진다고 선언하는 것이다. 실질적인 구현은 이를 구현한다고 선언하는 클래스에 맡긴다.
1 | interface Shape { |
위의 코드에서는 Rect
라는 클래스가 implements
키워드를 통하여 Shape
라는 인터페이스를 구현할 것이라고 선언하는 것이다. 일단 인터페이스를 구현하기로 했으면, 해당 인터페이스에 있는 프로퍼티 및 메소드를 전부 가지거나 구현해야 한다. 여기에서는 getArea
라는 메소드를 구현하지 않았으므로 에러가 발생한 모습이다.
덕 타이핑(Duck typing)
동적 타이핑 중에서 덕 타이핑이라는 것이 있다. JavaScript는 동적 타입 언어이므로 이 개념을 활용해서 코딩하게 된다.
1 | class Duck { |
만약 위와 같은 상황에서 Person
클래스에 quack
메소드가 구현되어 있지 않으면 어떻게 될까? 바로 런타임 에러를 발생시키며 프로그램이 종료될 것이다. 이와 같은 런타임 에러를 방지하기 위해서 메소드를 실행시키기 전에 검사할 수도 있지만 코드가 너무 장황해져 덕 타이핑의 장점이 사라진다.
TypeScript에서는 인터페이스를 활용하면 덕 타이핑을 할 때 메소드를 검사하지 않고도 런타임 에러를 막을 수 있다. TypeScript의 덕 타이핑은 어떤 객체가 특정 인터페이스에서 명시하는 메소드를 가지고 있다면 해당 객체가 그 인터페이스를 구현한 것으로 보는 것이다. 설명이 너무 장황하므로 코드 예제를 잠깐 보자.
1 | interface Quackable { |
Duck
클래스는 명시적으로 Quackable
인터페이스를 구현한다고 선언하였으므로 Quackable
객체만 인자로 받는 makeSomeNoiseWith
에 인자로 넘겨지는 것이 이상하지 않다. 그러나 Person
클래스는 조금 이상하다. 분명 Quackable
인터페이스를 구현한다고 선언하지 않았지만 문제없이 인자로 넘어간다.
이게 아까 말한대로 TypeScript에서의 덕 타이핑이다. 그냥 Quackable
에서 명시했던 quack
메소드만 구현되어 있다면 Quackable
객체로 보는 것이다. Person
클래스에서 quack
메소드를 제거하면 컴파일 에러가 난다. 개발자는 런타임에 메소드 검사를 하지 않고도 런타임 에러를 방지할 수 있다.
물론 조금 더 strict하게 타이핑을 하고 싶다면 implements
키워드를 사용하여 명시적으로 선언해주는 것도 여전히 좋은 방법이다. 참고로, Go에서도 이와 유사한 방식의 덕 타이핑을 활용할 수 있다. 이런 방식의 덕 타이핑은 구조적 타이핑(Structural typing) 이라고도 한다. 기회가 되면 덕 타이핑을 더 자세히 다뤄보는 포스트를 작성하도록 하겠다.
Optional 프로퍼티
인터페이스는 클래스와 매우 흡사한 모습을 가지고 있지만 주 용도는 다르다. 인터페이스로는 객체 인스턴스를 생성할 수 없으므로 주로 타입 검사를 위해서 활용된다. ES2015에 클래스는 있지만 인터페이스는 없다는 것이 그 사실을 방증한다. 그 차이가 Optional 프로퍼티에서 잘 드러난다. 인터페이스의 모든 프로퍼티 및 메소드는 구현하는 클래스에서 기본적으로 가지고 있어야 될 것들이지만, Optional 프로퍼티는 말 그대로 선택적으로 구현하는 프로퍼티다. 프로퍼티 식별자 뒤에 간단하게 ?
를 붙여서 사용한다.
1 | interface Shape { |
위 코드에서 알 수 있듯이, 인터페이스를 구현하는 클래스에서는 Optional 프로퍼티를 가지고 있지 않더라도 에러가 발생하지 않는다.
Indexable
JavaScript의 객체는 프로퍼티 접근자(Property accessor)를 두 가지 제공한다. 하나는 점 표기법(Dot notation)이고, 다른 하나는 괄호 표기법(Bracket notation)이다. 기본적으로 점 표기법을 자주 사용하기는 하지만, 동적으로 프로퍼티에 접근하려는 경우 문자열으로 프로퍼티에 접근할 수 있는 괄호 표기법을 사용하기도 한다.
그러나 TypeScript는 괄호 표기법으로 프로퍼티에 접근하려고 하면 애로사항이 꽃 핀다. 다음의 코드를 보자.
1 | const dict = { |
예제가 조금 이상하긴 하지만 JavaScript 개발을 조금이라도 해봤다면 동적으로 프로퍼티에 접근하는 상황이 종종 있었을 것이다. JavaScript에서 이 코드는 정상적으로 동작한다. 그러나 TypeScript에서는 dict의 프로퍼티를 동적으로 접근하는 부분(console.log
인자 부분)에서 error: Index signature of object type implicitly has an 'any' type.
이라는 에러가 나면서 컴파일이 되지 않는다.1 TypeScript가 괄호 표기법을 제공하지 않는 것은 아니지만, 동적인 키 값을 사용하게 되면 에러가 발생한다.
에러 원인은 간단하다. 어떤 타입의 프로퍼티에 접근하는 지 알 수 없기 때문에 리턴 값을 묵시적으로 any
타입으로 변환하므로 에러를 띄우는 것이다. 이를 해결하기 위해서는 noImplicitAny
값을 false
로 바꾸던지, 객체 자체를 Indexable 하게 만드는 방법밖에 없다. 객체를 Indexable 하게 만드려면 인덱스 시그니처(Index signature)를 사용하면 된다.
1 | interface Indexable { |
위에 새로 추가된 인터페이스 내부 [key: string]: any
라는 문장이 바로 인덱스 시그니처다. 괄호 표현법과 함께 string
으로 접근하게 되면 any
타입의 무언가를 돌려줄 것이라는 의미다. 여기서는 묵시적으로 any
타입을 리턴하지 않으므로 에러가 발생하지 않는다. 하지만 키 값으로 number
나 Symbol
같은 다른 타입을 넘기게 되면 다시 에러가 발생하기 때문에 마찬가지로 필요하다면 인덱스 시그니처를 정의해야 한다.
함수 인터페이스
TypeScript의 인터페이스는 함수의 인터페이스를 정의할 수도 있다.
1 | interface numberOperation { |
문법은 식별자 없이, 받아야할 인자의 타입과, 리턴 타입만을 표기하면 된다. 쉽게 예상할 수 있겠지만, 이 인터페이스를 구현하는 함수는 반드시 정의했던 타입의 인자를 받아 정의했던 타입을 리턴해야만 에러없이 컴파일이 된다.
multiply
처럼, 정의했던 인터페이스대로 구현된 함수는 굳이 타입을 명시할 필요는 없다. 이상한 타입만 명시하지 않으면 된다. 여기서 이상한 타입이라 함은 any
와 애초에 인터페이스에서 선언했던 number
를 제외한 타입들을 말한다.
타입을 명시하지 않으면 함수를 실제로 사용할 때 인자로 이상한 타입을 넘겨도 될 것 같지만, 그렇게 하면 타입이 맞지 않아 에러가 뜬다.
생성자 인터페이스
JavaScript에서 함수는 일급 시민이므로 다른 함수에 인자로 넘길 수 있다. 생성자도 마찬가지로 함수이므로 인자로 넘기는 것이 가능하다.
1 | class Dog { |
createAnimal
이라는 함수의 윤리적인 이슈에 대해서 다루기엔 너무 양이 길테니 일단 넘어가도록 하자. 이 예제는 그냥 JavaScript 코드인데 문제없이 잘 돌아간다. 간단한 예제지만 TypeScript에서 구현하기가 간단하지 않다. 가장 큰 문제는 createAnimal
의 첫 번째 인자로 받는 생성자 함수의 타입을 어떻게 정의하냐는 문제다. 일단 생성자는 함수이니, Function
으로 정의하면 될 거 같다. 한 번 시도해보자.
1 | class Dog { |
하지만 위의 코드는 컴파일이 안된다. TypeScript에서는 new
와 함께 일반 함수를 호출할 수 없기 때문이다. 따라서 TypeScript가 생성자로 인식할 만한 어떤 타입을 써주어야 한다. 이 때 new
라는 키워드를 이용해서 생성자의 인터페이스를 정의할 수 있다.
1 | interface Animal { |
당연히 생성자는 그것으로 인해 생성된 객체 인스턴스와 다른 타입이다. 이 코드에서는 객체 인스턴스가 Animal
타입, 그리고 생성자는 AnimalConstructor
타입이다. AnimalConstructor
는 new
라는 키워드와 함께 함수 인터페이스 문법을 사용하는데 이것이 TypeScript에서 생성자를 정의하는 문법이다. 앞에 new
키워드가 들어간다는 걸 제외하면, 일반 함수 인터페이스 문법과 다른 점이 없다. 다만 Animal
타입을 리턴한다는 것을 명시적으로 선언해줘야 한다.
하이브리드 타입
하이브리드 타입은 함수이기도 하면서 객체이기도 한 인터페이스다. 예를 들면, jQuery의 $
는 객체이기도 하면서 쿼리 셀렉터로 기능하기도 하는 함수이기도 하다. 이 경우에 jQuery의 인터페이스를 정의하면 다음과 같이 쓸 수 있을 것이다.2
1 | interface jQueryElement { |
위 처럼 함수 인터페이스의 문법과 일반 인터페이스 프로퍼티의 문법을 함께 사용할 수 있다. 사용하려는 JavaScript 라이브러리가 jQuery처럼 함수이면서도 객체로 구현된 경우, 이런 형태로 인터페이스를 정의할 수 있을 것이다.
그 외
TypeScript의 인터페이스는 위에 설명한 것 이외에도 많은 기능을 갖추고 있다. 먼저 인터페이스가 인터페이스를 extends
키워드를 통해 확장할 수 있으며, 인터페이스끼리 다중 상속도 가능하다. 또한 인터페이스를 통해 클래스도 확장이 가능하다. 이런 기능들은 굳이 예제를 들어 설명해야할 정도로 이해하기 어려운 기능은 아니므로 따로 지면을 할애하지는 않았다. 실은 좀 귀찮아서..
다음 글 - TypeScript: 함수(Function)