-
📌 Interface
TypeScript의 핵심 원칙 중 하나는 타입 검사가 값의 형태에 초점을 맞추고 있다는 것이다. 이를 "덕 타이핑(duck typing)" 혹은 "구조적 서브타이핑 (structural subtyping)"이라고도 한다.
인터페이스는 이런 타입들의 이름을 짓는 역할을 하고, 코드 안의 계약을 정의하는 것뿐만 아니라 프로젝트 외부에서 사용하는 코드의 계약을 정의하는 강력한 방법이다.
interface LabeledValue { // 인터페이스명은 대문자로 짓는다 label: string; } function printLabel(labeledObj: LabeledValue) { console.log(labeledObj.label); } let myObj = {size: 10, label: "Size 10 Object"}; printLabel(myObj);
printLabel
함수는string
타입 label을 갖는 객체를 하나의 매개변수로 가진다. 컴파일러는 최소한 필요한 프로퍼티가 있는지와 타입이 잘 맞는지만 검사한다. 함수에 전달된 객체가 나열된 요구 조건을 충족하면, 허용된다. 그리고 프로퍼티들의 순서를 요구하지 않는다.📌 선택적 프로퍼티
인터페이스의 모든 프로퍼티가 필요한 것은 아니다. 어떤 조건에서만 존재하거나 없을 수도 있다.
선택적 프로퍼티들은 객체 안의 몇 개의 프로퍼티만 채워 함수에 전달하고자 할 때 유용하다.
interface SquareConfig { color?: string; width?: number; } function createSquare(config: SquareConfig): {color: string; area: number} { let newSquare = {color: "white", area: 100}; if (config.color) { newSquare.color = config.color; } if (config.width) { newSquare.area = config.width * config.width; } return newSquare; } let mySquare = createSquare({color: "black"});
선택적 프로퍼티는 선언에서 프로퍼티 이름 끝에
?
를 붙여 표시한다.
주의할 점은, 선택적 프로퍼티는 있을 수도 있고, 없을 수도 있다는 의미이기에Union
타입의string | undefined
선언과 마찬가지이다. 그러므로undefined
일 경우로 추론될 수 있기에 타입 가드를 사용해주어야 한다.📌 읽기 전용 프로퍼티
인터페이스로 객체를 처음 생성할 때만 값을 할당하고 그 이후에는 변경할 수 없는 속성을 의미한다.
속성 앞에
readonly
키워드를 붙여주면 된다.interface Point { readonly x: number; readonly y: number; }
읽기 전용 프로퍼티로 설정이 되면 객체 리터럴을 할당하여 Point를 생성 및 할당을 한 이후에는 수정할 수 없다.
let p1: Point = { x: 10, y: 20 }; p1.x = 5; // 오류!
🤔 readonly vs const
readonly
와const
는 처음 초기화 할 때만 값을 선언하고 그 이후에는 수정할 수 없다는 특징이 유사하지만 차이점이 있다. 변수는const
를 쓰고 프로퍼티는readonly
를 사용된다.📌 초과 프로퍼티 검사
맨 상단의 인터페이스 예시에서 타입스크립트의 타입 검사가 값의 형태에 초점을 맞추고 있다는 특징으로 최소한 필요한 프로퍼티가 있는지와 타입이 잘 맞는지만 검사한다고 했었다. 하지만 타입스크립트에서 객체 리터럴을 다른 변수에 직접 할당할 때나 인수로 전달할 때는 초과 프로퍼티 검사를 하는데, 해당 인터페이스에 정의되지 않은 프로퍼티가 있다면 오류가 발생한다.
interface LabeledValue { label: string; } function printLabel(labeledObj: LabeledValue) { console.log(labeledObj.label); } //let myObj = {size: 10, label: "Size 10 Object"}; printLabel({size: 10, label: "Size 10 Object"}); // Argument of type '{ size: number; label: string; }' is not assignable to parameter of type 'LabeledValue'. // Object literal may only specify known properties, and 'size' does not exist in type 'LabeledValue'.
이 검사를 피하는 방법은 정말 간단하다.
1️⃣ 첫 번째 방법은 맨 위의 예시처럼 객체를 다른 변수에 할당하는 것이다.
interface LabeledValue { label: string; } ... let myObj = {size: 10, label: "Size 10 Object"}; printLabel(myObj);
2️⃣ 두 번째 방법은 타입 단언을 사용하는 것이다.
interface LabeledValue { label: string; } ... //let myObj = {size: 10, label: "Size 10 Object"}; printLabel({size: 10, label: "Size 10 Object"} as LabeledValue);
3️⃣ 하지만 특별한 경우에, 추가 프로퍼티가 있음을 확신한다면, 문자열 인덱스 서명(string index signatuer)을 추가하는 것이 더 나은 방법이 될 수 있다.
interface LabeledValue { label: string; [propName: string]: any; }
위 예시를 해석해 보자면
LabeledValue
가 여러 프로퍼티를 가질 수 있고, 그 프로퍼티들이 label이 아니라면, 그들의 타입은 중요하지 않다. 정리하자면 이 초과 프로퍼티 검사는 웬만하면 "피하는" 방법을 시도하지 않는 것이 좋다. 초과 프로퍼티 에러의 대부분은 실제 버그이기 때문에 타입 정의를 수정하는 것이 더 올바르다.📌 함수 타입
인터페이스는 JavaScript 객체가 가질 수 있는 넓은 범위의 형태를 기술할 수 있다. 프로퍼티로 객체를 기술하는 것 외에, 인터페이스는 함수 타입을 설명할 수 있다. 인터페이스로 함수 타입을 기술하기 위해, 인터페이스에 호출 서명 (call signature)를 전달한다. 이는 매개변수 목록과 반환 타입만 주어진 함수 선언과 비슷하다.
interface SearchFunc { (source: string, subString: string): boolean; } // 매개 변수 이름이 인터페이스와 일치할 필요가 없다. // 또한 타입 추론을 통해 선언할 함수에 타입을 굳이 쓸 필요가 없다. let mySearch: SearchFunc; mySearch = function(src: string, sub: string): boolean { let result = src.search(sub); return result > -1; }
📌 인덱서블 타입
인덱서블 타입이란, 인덱싱이 가능한 자료형을 의미한다. 이것은 특정 자료형의 변수를 배열 혹은 딕셔너리로 구현할 때 유용하다. 이러한 인덱서블 타입은 index signature를 가져야 하며, 인덱스 시그니쳐는
number
혹은string
이어야 한다.interface StringArray { [index: number]: string; } let myArray: StringArray; myArray = ["Bob", "Fred"]; let myStr: string = myArray[0];
위의 예시에서
StringArray
인터페이스의 인덱스 서명은StringArray
가number
로 색인화(indexed)되면string
을 반환할 것을 나타낸다.
인덱스 시그니처는 인터페이스에 '정의되지 않은 속성' 들을 유기적으로 사용할 때 유용하다는 장점이 있지만 모든 프로퍼티들이 반환 타입과 일치하도록 강제한다.interface NumberDictionary { [index: string]: number; length: number; // 성공, length는 숫자입니다 name: string; // 오류, `name`의 타입은 인덱서의 하위타입이 아닙니다 }
하지만, 인덱스 시그니처가 프로퍼티 타입들의 합집합이라면 다른 타입의 프로퍼티들도 허용할 수 있다.
interface NumberOrStringDictionary { [index: string]: number | string; length: number; // 성공, length는 숫자입니다 name: string; // 성공, name은 문자열입니다 }
📌 클래스 타입
타입스크립트로 클래스가 특정 계약(contract)을 충족시키도록 명시적으로 강제할 수 있다.
인터페이스로 클래스를 정의하는 경우,implements
키워드를 사용해 클래스 정의 옆에 명시해 주면 된다.implements
하면 클래스의 프로퍼티 구조는 반드시ClockInterface
에 정의된 대로 따라야 한다.interface ClockInterface { currentTime: Date; } class Clock implements ClockInterface { currentTime: Date = new Date(); constructor(h: number, m: number) { } }
아래 예제처럼 클래스에 구현된 메서드를 인터페이스 안에서도 기술할 수 있다.
interface ClockInterface { currentTime: Date; setTime(d: Date): void; } class Clock implements ClockInterface { currentTime: Date = new Date(); setTime(d: Date) { this.currentTime = d; } constructor(h: number, m: number) { } }
📌 구성 시그니처
구성 시그니처는
new 클래스()
생성자 함수 타입 구조를 정의하는 것이다. 이를 응용해 함수 매개변수를 클래스를 받아 대신 초기화할 수 있다.interface ICatConstructor { new (name: string): Cat; // 구성 시그니처 } class Cat { constructor(public name: string) {} // 생성자 함수 } // 클래스를 인수로 받고 대신 초기화 해주는 함수 function makeKitten(c: ICatConstructor, n: string) { return new c(n); // ok } const kitten = makeKitten(Cat, 'Lucy'); console.log(kitten.name); // Lucy
📌 인터페이스 확장
클래스처럼 인터페이스도 확장(extend)이 가능하다.
interface Shape { color: string; } interface Square extends Shape { sideLength: number; } let square = {} as Square; square.color = "blue"; square.sideLength = 10;
또한 여러 개의 인터페이스를 확장할 수 있어, 모든 인터페이스의 조합을 만들어낼 수 있다.
interface Shape { color: string; } interface PenStroke { penWidth: number; } interface Square extends Shape, PenStroke { sideLength: number; } let square = {} as Square; square.color = "blue"; square.sideLength = 10; square.penWidth = 5.0;
[참고 자료👇]
https://typescript-kr.github.io/pages/interfaces.html
https://inpa.tistory.com/entry/TS-📘-타입스크립트-인터페이스-💯-활용하기
728x90'TypeScript' 카테고리의 다른 글
Object key를 Type으로 만들기 (0) 2023.08.17 타입스크립트 설정 파일 (0) 2023.06.25 기본 타입 (0) 2022.11.28 Primitive Types (0) 2022.11.10 TypeScript Types VS JavaScript Types (0) 2022.11.10 댓글