인터페이스 (Interface)
소개
TypeScript의 핵심 원리 중 하나는 type-checking이 값의 형태(shape)에 초점을 맞춘다는 것입니다. 이것은 때때로 “duck typing“또는 “structural subtyping“라고도 합니다. TypeScript에서 인터페이스는 이러한 타입의 이름을 지정하는 역할을 하며 외부의 코드와 여러분의 코드의 약속을 정의하는 강력한 방법입니다.
첫번째 인터페이스
인터페이스의 작동 원리를 이해하기위해 간단한 예제를 보겠습니다.
|
|
type-checker는 printLabel
에 대한 호출을 확인합니다. printLabel
함수에 전달된 객체는 string 타입의 label
이라는 프로퍼티가 하나 있습니다. 객체는 실제로 이것보다 더 많은 프로퍼티를 가지고 있을수 있지만 컴파일러는 적어도 필요한 것들이 있는지와 타입만 일치하는지 확인합니다. 하지만 TypeScript가 관대하지 않는 경우도 있습니다.
이번에는 인터페이스를 사용하여 label
프로퍼티가 문자열을 가져야 한다는 요구 사항을 설명하는, 동일한 예제를 다시 작성해 보겠습니다.
|
|
LabelledValue
인터페이스는 이전 예제의 요구 사항을 충족하는 이름입니다. 여전히 string 타입의 label
이라는 하나의 프로퍼티가 있다는 것을 보여줍니다. printLabel
에 전달한 객체가 다른 언어와 같이 이 인터페이스를 구현한다고 명시 적으로 말할 필요가 없었습니다. 여기서 중요 것은 형태일 뿐입니다. 함수에 전달된 객체가 요구 사항을 충족하면 됩니다.
Type-checker는 프로퍼티가 어떤 순서로든 상관하지 않으며 인터페이스에 필요한 프로퍼티가 있고 정확한 타입이 필요하다는 점을 지적한다는 것이 중요합니다.
선택적 프로퍼티
인터페이스의 모든 프로퍼티가 필수일 필요는 없습니다. 일부는 특정 조건 하에서 존재하거나 전혀 존재하지 않을 수도 있습니다. 이러한 Optional propery는 “option bags”와 같이 두개의 프로퍼티가 있는 객체를 함수에 전달하는 패턴을 생성할때 많이 사용됩니다.
아래의 예제는 이러한 패턴의 일반적인 형태입니다.
|
|
Optional propery를 가지는 인터페이스는 다른 인터페이스와 비슷하게 작성되며, 단지 프로퍼티 선언의 이름 끝에 ?
가 추가 됩니다.
Optional propery의 장점은 인터페이스의 일부가 아닌 프로퍼티의 사용을 방지하고 사용 가능한 프로퍼티을 열거할 수 있다는 것입니다. 예를 들어 createSquare
에서 color
프로퍼티의 이름을 잘못 입력했다면 다음과 같은 오류 메시지가 표시됩니다.
|
|
읽기전용 프로퍼티
일부 프로퍼티는 객체를 처음 만들 때만 수정할 수 있어야 할 수 있습니다. 프로퍼티 이름 앞에 readonly
를 두어 읽기전용으로 지정할 수 있습니다.
|
|
객체 리터럴을 할당하여 Point
를 생성할 수 있습니다. 할당 후에 x
와 y
는 바꿀 수 없습니다.
|
|
TypeScript에는 Array<T>
와 동일하지만 모든 수정가능한 메서드가 제거된 ReadonlyArray<T>
타입이 있으므로 배열 생성 후 배열을 변경하지 않도록 할 수 있습니다.
|
|
위 코드의 마지막 줄을 보면 ReadonlyArray ro
를 일반 배열로 다시 할당하는 것조차도 에러가 발생하는 것을 볼수 있습니다. 하지만 아래와 같이 Type assertion으로 Override
할 수 있습니다.
|
|
readonly
vs const
readonly
와 const
중에 어떤걸 사용할지 결정하는 가장 쉬운 방법은 변수 또는 프로퍼티 중 어떤 형태를 사용하느냐에 따라 결정합니다. 변수는 const
를 사용하고 프로퍼티는 readonly
를 사용합니다.
프로퍼티 접근 체크
첫번째 예제는 단지 {label : string; }
가 필요한 상황에서 {size: number; label: string; }
이 같은 객체를 함수의 파라미터로 넘기는 것을 보았습니다. 그리고 Optional Property
에 대해서도 알게 되었고 “option bags” 패턴에 유용하다는 것을 설명 했습니다.
그러나 두 가지를 순수하게 결합하여 JavaScript에서와 같은 방법으로 사용할 수 있습니다. 예를 들어, createSquare
를 사용한 마지막 예제를 보겠습니다.
|
|
createSquare
에 주어진 파라미터는 color
대신 colour
라고 쓰여 있습니다.
입력한 파라미터의 width
속성은 호환 가능하고 color
속성은 없으며, 여분의 colour
속성은 중요하지 않으므로 이 프로그램이 올바르게 작성되었다고 주장할 수 있습니다.
그러나 TypeScript는 이 코드에 버그가 있음을 알수 있습니다. 객체 리터럴은 다른 변수에 할당하거나 파라미터로 전달될 때 특별한 처리를 받아 프로퍼티 접근 Checking을 받습니다. 객체 리터럴에 “대상 타입”에 없는 속성이 있으면 오류가 발생합니다.
|
|
이 *Check를 피하는 방법은 아주 간단합니다. 가장 쉬운 방법은 Type assertion을 사용하는 것입니다.
|
|
그러나 객체에 다른 용도로 사용되는 몇 가지 추가 속성이 있다고 확신하는 경우 string에 대한 Index signature을 추가하는 것이 더 나은 방법 일 수 있습니다. SquareConfigs
가 위의 타입과 함께 color
와width
속성을 가질 수 있지만 다른 속성도 가질 수 있다면 다음과 같이 정의할 수 있습니다.
|
|
Index signature에 대해서는 나중에 설명 하겠지만 여기서는 SquareConfig
가 여러 속성을 가질 수 있으며 color
또는 width
가 아닌 다른 프로퍼티들의 타입은 중요하지 않습니다.
조금 놀랍게도 Check를 피하는 마지막 방법은 다른 변수에 객체를 할당하는 것입니다. 아래 코드의 squareOptions
는 초과된 프로퍼티를 Check 하지 않기 때문에 컴파일러는 에러를 발생하지 않습니다.
|
|
하지만 위의 코드와 같이 이러한 Check를 피하는 시도를 하지 않아야 합니다. 메서드와 상태를 유지하는 더 복잡한 객체 리터럴의 경우 이러한 기법을 염두에 두어야할 수도 있지만 대부분의 초과 프로퍼티 오류는 실제로 버그입니다. 즉, Option bag 과 같은 것에 대해 초과 속성 검사 문제가 발생한다면, 타입 선언의 일부를 수정해야할 수도 있습니다. 이 경우, color
또는 colour
속성을 가진 객체를createSquare
에 전달하면, 그것을 반영하기 위해SquareConfig
의 정의를 수정해야합니다.
함수 타입
인터페이스는 JavaScript 객체가 취할 수있는 다양한 형태로 설명할 수 있습니다. 프로퍼티을 가진 객체를 설명하는 것 외에도 인터페이스는 함수 타입을 정의할 수 있습니다.
TypeScript는 인터페이스에 있는 함수 타입을 정의하기 위해 Call signature 인터페이스를 제공합니다. 이것은 주어진 파라미터 목록과 리턴 타입만 있는 함수 선언과 같습니다. 파라미터 목록의 각 파라미터는 이름과 타입이 모두 필요합니다.
|
|
일단 정의되면 우리는 다른 인터페이스 처럼 이 함수 타입 인터페이스를 사용할 수 있습니다. 아래 코드는 함수 타입의 변수를 만들고 같은 타입의 함수를 할당하는 방법을 보여줍니다.
|
|
함수 타입이 올바른지 확인하기 위해 파라미터의 이름이 같을 필요는 없습니다. 예를 들어 위의 예제를 다음과 같이 작성할 수 있습니다.
|
|
함수 파라미터는 한 번에 하나씩 , 서로 대응하는 각 해당 파라미터의 위치와 타입이 Check됩니다. 타입을 전혀 지정하지 않은 경우 TypeScript의 Contextual typing이 함수 SearchFunc
타입의 파라미터에 직접 지정되므로 파라미터 타입을 유추할 수 있습니다. 또한 함수 표현식의 리턴 타입은 반환하는 값 (여기에는 false
및 true
)에 의해 암묵적으로 유추됩니다. 함수 표현식이 숫자나 문자열을 반환했다면, Type-checker는 리턴 타입이 SearchFunc
인터페이스에서 기술된 리턴 타입과 일치하지 않는다고 경고했을 것입니다.
|
|
Indexable 타입
함수 타입을 설명하기 위해 인터페이스를 사용하는 방법과 마찬가지로, [10]
, 또는 ageMap[ "daniel"]
처럼 “인덱스”할 수있는 타입을 정의할 수 있습니다. 인덱싱 가능 유형에는 인덱싱할 때 대응되는 리턴 타입과 함께 객체에 대해 인덱싱하는 데 사용할 수있는 타입을 설명하는 Index signature가 있습니다. 예를 들어 보겠습니다.
|
|
위에서 Index signature를 갖는 StringArray
인터페이스가 있습니다. 이 Index signature는 StringArray
가 number
로 인덱스 될 때 string
을 리턴한다는 것을 나타냅니다.
지원되는 Index signature에는 문자열과 숫자의 두 가지 타입이 있습니다. 두 가지 타입의 인덱서를 모두 지원하는 경우에는 숫자 인덱서에서 반환된 형태는 문자열 인덱서에서 반환된 형태의 하위 형태이어야 합니다. 이는 number
를 사용하여 색인을 생성할 때 JavaScript가 객체로 색인하기 전에 실제로 JavaScript를 문자열로 변환하기 때문입니다. 즉, 100
(number
)로 인덱싱하는 것은 "100"
(string
)으로 인덱싱하는 것과 동일하므로 두 가지가 일관성이 있어야합니다.
|
|
문자열 Index signature은 “Dictionary” 패턴을 설명하는 강력한 방법이지만 모든 프로퍼티가 리턴 타입과 일치 해야 합니다. 이것은 문자열 인덱스 obj.property
가 obj["property"]
로 사용 가능하다는 것을 선언되기 때문입니다. 다음 예제에서 name
의 형식은 문자열 인덱스의 형식과 일치하지 않으며 Type-checker에서 오류가 발생합니다.
|
|
마지막으로 인덱스에 할당하지 못하도록 Index signature을 읽기 전용으로 만들 수 있습니다.
|
|
Index signature이 읽기 전용이기 때문에 myArray[2]
에 값을 설정할 수 없습니다.
클래스 타입
Interface 구현
C# 및 Java와 같은 언어로 인터페이스를 사용하는 가장 일반적인 방법 중 하나는 클래스가 특정 계약을 준수하도록 명시적으로 적용하는 것입니다. TypeScript에서도 가능합니다.
|
|
아래 예제에서 setTime과 마찬가지로 클래스에 구현된 인터페이스의 메소드를 설명할 수도 있습니다.
|
|
인터페이스는 클래스의 public
과 private
이 아니라 public
만을 기술할 수 있습니다. 그렇기 때문에 클래스를 사용하여 클래스 인스턴스의 private
에 특정 타입이 있는지 확인할 수 없습니다.
클래스의 Static과 Instance의 차이점
클래스와 인터페이스로 작업할 때 클래스에는 Static 측면의 타입과 Instance 측면의 타입이라는 두 가지 유형이 있음을 명심하세요. Construct signature를 사용하여 인터페이스를 만들고 이 인터페이스를 Implements하는 클래스를 만들려고 하면 오류가 발생할 수 있습니다.
|
|
이는 클래스가 인터페이스를 Implements할 때 클래스의 Instance 적인 면만 검사하기 때문입니다. 생성자는 Static 측면에 있으므로 이 Check에 포함되지 않습니다.
대신 클래스의 Static인 측면에서 직접 작업해야 합니다. 아래 예제에서 우리는 생성자를 위한 ClockConstructor
와 인스턴스 메서드를 위한ClockInterface
라는 두 개의 인터페이스를 정의합니다. 편의상 우리는 전달된 타입의 인스턴스를 생성하는 생성자 함수 createClock
도 같이 정의합니다.
|
|
createClock
의 첫번째 파라미터는 ClockClonstructor
타입이고 createClock(AnalogClock, 7, 32)
에 AnalogClock
이 올바른 Construct signature를 가지고 있는지를 검사합니다.
인터페이스 확장
클래스와 마찬가지로 인터페이스는 서로를 확장할 수 있습니다. 이렇게 하면 한 인터페이스의 구성원을 다른 인터페이스로 복사할 수 있으므로 인터페이스를 재사용 가능한 구성 요소로 분리하는 방법을 보다 유연하게 사용할 수 있습니다.
|
|
인터페이스는 여러 인터페이스를 확장하고 모든 인터페이스를 조합하여 만들 수 있습니다.
|
|
결합 (Hybrid) 타입
이전에 언급했듯이 인터페이스는 실제 JavaScript에서 제공되는 풍부한 타입을 나타낼 수 있습니다. JavaScript의 역동적이고 유연한 특성으로 인해 위에 설명된 일부 타입의 조합으로 작동하는 객체가 종종 발생할 수 있습니다.
이러한 예는 추가 프로퍼티을 사용하여 함수와 객체의 역할을 모두 수행하는 객체입니다.
|
|
타사 JavaScript 라이브러리와 상호 작용할 때 타입 모양을 완전히 설명하려면 위와 같은 패턴을 사용해야 할 수도 있습니다.
클래스 확장 인터페이스
인터페이스 유형이 클래스 타입을 확장할 때 클래스 타입을 상속하지만 Implements된 내용은 상속되지 않습니다. 마치 인터페이스가 implements를 제공하지 않고 클래스의 모든 멤버를 선언 한 것과 같습니다. 인터페이스는 기본 클래스의 private 및 protected 멤버도 상속합니다. 즉, private 또는 protected 멤버가 있는 클래스를 확장하는 인터페이스를 만들면 해당 인터페이스 유형은 해당 클래스 또는 해당 클래스의 하위 클래스에서만 implements 할 수 있습니다.
이는 많은 상속 계층이 있지만 코드가 특정 프로퍼티가 있는 하위 클래스에서만 작동하도록 지정하려는 경우에 유용합니다. 서브 클래스는 기본 클래스에서 상속하는 것 외에 관련 될 필요가 없습니다.
|
|
위의 예제에서, SelectableControl
은 private state
속성을 포함하여 Control
의 모든 멤버를 포함합니다. state
는 private 멤버이기 때문에 Control
의 자손만 SelectableControl
을 구현할 수 있습니다. 왜냐하면 Control
의 자손들만이 같은 선언에서 유래된 private state
멤버를 가질 것이기 때문입니다. 이는 private 멤버들이 호환 가능해야 한다는 요구 사항입니다.
Control
클래스 안에서 SelectableControl
인스턴스를 통해 private state
멤버에 접근할 수 있습니다. 효율적으로 SelectableControl은 select
메소드를 가진 것으로 알려진 Control
과 같은 역할을 합니다. Button
과 TextBox
클래스는SelectableControl
의 하위 타입입니다. (왜냐하면 둘 다 Control
을 상속 받았고 select
메소드를 가졌기 때문입니다), 하지만 Image
클래스와 Location
클래스는 그렇지 않습니다.
이 내용은 나중에 참고하기 위해 제가 공부하며 정리한 내용입니다.
의역, 오역, 직역이 있을 수 있음을 알려드립니다.
This post is a translation of this original article [https://www.typescriptlang.org/docs/handbook/interfaces.html]
참고
- TypeScript 핸드북 1 - 기본 타입
- TypeScript 핸드북 2 - 변수 선언
- TypeScript 핸드북 3 - 인터페이스
- TypeScript 핸드북 4 - 클래스
- TypeScript 핸드북 5 - 함수
- TypeScript 핸드북 6 - Generic
- TypeScript 핸드북 7 - Enum
- TypeScript 핸드북 8 - 타입 유추
- TypeScript 핸드북 9 - 타입 호환성
- TypeScript 핸드북 10 - 고급 타입
- TypeScript 핸드북 11 - Symbol
- TypeScript 핸드북 12 - Iterator와 Generator