인터페이스는 상호 간에 정의한 약속 혹은 규칙
이다. 타입스크립트에서 인터페이스는 보통 아래와 같은 경우에 규칙을 정의할 수 있다.
아래 예제를 통해서 타입스크립트의 인터페이스가 어떻게 동작하는지 알아보자.
function printLabel(labeledObj: { label: string }) {
console.log(labeledObj.label)
}
const myObj = { size: 10, label: 'size 10 object' };
printLabel(myObj);
printLabel
호출을 확인한다.printLabel
함수는 string
타입 label
을 갖는 개체를 하나의 매개변수로 가진다.label
프로퍼티 이외에 size
프로퍼티도 가지고 있지만 컴파일러는 최소한 필요한 프로퍼티가 있는지와 타입이 잘 맞는지
만 검사한다.같은 예제를 인터페이스를 사용해서 다시 작성해보자.
interface LabeledValue {
label: string;
}
function printLabel(labeledObj: LabeledValue) {
console.log(labeledObj.label)
}
const myObj = { size: 10, label: 'size 10 object' };
printLabel(myObj);
LabeledValue
인터페이스는 이전 예제의 요구사항({ label: string })과 동일하지만 이름으로 사용할 수 있다.printLabel
함수 호출 시 전달한 myObj
객체가 인터페이스와 동일하게 구현할 필요는 없다.printLabel
함수 호출 시 전달한 myObj
객체가 인터페이스에 나열된 요구 조건을 충족하면 허용된다.인터페이스의 모든 프로퍼티가 필요하지 않은 경우가 존재한다. 선택적 프로퍼티는 객체 내부의 몇 개의 프로퍼티만 채워 함수에 전달하는 option bags
같은 패턴을 만들 때 유용한다.
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
const newSquare = { color: 'white', area: 100 };
if(config.color) {
newSquare.color = config.color;
// Error: Property 'clor' does not exist on type 'SquareConfig'
newSquare.color = config.clor;
}
if(config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}
let mySquare = createSquare({ color: 'black' });
선택적 프로퍼티는 선언할 때 프로퍼티 이름 끝에 ?
를 붙여서 표시한다.
선택적 프로퍼티의 이점은 인터페이스에 속하지 않는 프로퍼티의 사용을 방지하고, 사용 가능한 속성을 기술한다. 예를 들어 createSquare 내부의 color 프로퍼티 이름을 잘 못 입력하면 오류 메시지로 알려준다.
우아한 타입스크립트 에서는 아래와 같은 경우에 선택적 타입 대신 유니온 타입을 사용해서 명시적으로 사용을 제한시킨다.
interface Result {
data?: string;
error?: Error;
loading: boolean;
}
declare function getUserDatas(): Result;
const result = getUserDatas();
result.data; // string | undefined
result.error; // Error | undefined
result.loading; // boolean
if(result.data) {
result.error; // Error | undefined
result.loading // boolean
}
보통의 경우에 정상적으로 외부 데이터를 가져와서 data
가 존재하면 error
는 존재할 수 없다. 하지만 위 예제는 에러에 접근이 가능하다는 문제가 발생한다.
type Result =
| { loading: true }
| { data: string; loading: false }
| { error: Error; loading: false; }
declare function getUserDatas(): Result;
const result = getUserDatas();
if('data' in result) {
result.data;
result.loading;
// error, Property error dose not exist on Type { data: string; loading: false }
result.error;
}
유니온 타입을 사용하면 요청 중, 요청 성공, 요청 실패
상태에 따라 접근할 수 있는 데이터를 명확하게 구분해서 사용할 수 있다.
개발을 하다보면 객체의 일부 프로퍼티의 값을 수정 불가능하게 설정해야하는 경우가 존재한다. 프로퍼티 앞에 readonly
를 넣어서 이를 지정할 수 있다.
interface Point {
readonly x: number;
readonly y: number;
}
const point: Point = { x: 10, y: 20 };
point.x = 20; // readonly로 수정을 시도하면 에러가 발생한다.
타입스크립트는 배열 생성 후 내부 값들을 수정할 수 없도록 할 수 있다.
const num: number[] = [1, 2, 3, 4];
const readonlyNum: ReadonlyArray<number> = [1, 2, 3, 4];
num[0] = 10; // 성공
num.push(10); // 성공
num.length = 100; // 성공
readonlyNum[0] = 10; // 오류
readonlyNum.push(10); // 오류
readonlyNum.length = 100; // 오류
num = readonlyNum; // 오류
예제 마지막 줄에서 ReadonlyArray
타입의 배열은 일반 배열에 재할당 할 수 없다. 타입 단언으로 오버라이드는 가능하다.
num = readonlyNum as number[];
인터페이스 함수는 아래와 같이 정의할 수 있다.
interface SearchFunc {
(source: string, subString: string): boolean;
}
함수 타입 인터페이스는 다른 인터페이스처럼 사용할 수 있다. 함수 타입의 변수를 만들고 같은 타입의 함수를 값으로 할당할 수 있다.
let mySearch: SearchFunc;
mySearch = function(sourceL string, subString: string): string {
let result = source.search(subString);
return result > -1;
}
함수의 타입 검사를 위해서 매개변수의 이름이 같은 필요는 없다. 함수의 매개변수는 같은 위치에 대응되는 매개변수끼리 한번에 하나씩 검사한다.
let mySearch: SearchFunc;
mySearch = function(src: string, sub: string): boolean {
let result = src.search(sub);
return result > -1;
}
mySearch
변수에 SearchFunc
타입을 할당했기 때문때 타입을 직접 입력하지 않아도 인수와 반환 타입을 추론
할 수 있다.
let mySearch: SearchFunc;
mySearch = function(src, sub) {
let result = src.search(sub);
return result > -1;
}
만약 함수가 숫자나 문자열과 같은 다른 타입을 반환한다면 인터페이스에 정의된 타입과 일치하지 않다고 에러를 발생시킨다.
let mySearch: SearchFunc;
// error: Type '(src: string, sub: string) => string' is not assignable to type 'SearchFunc'.
// Type 'string' is not assignable to type 'boolean'.
mySearch = function(src, sub) {
let result = src.search(sub);
return "string";
};
지금까지 사용한 인터페이스는 직접 속성의 타입을 지정했다. 그러나 객체, 배열과 같은 경우 속성이 많이 들어거나 어떤 속성이 들어올 지 확정지을 수 없는 경우 하나하나 타입을 지정할 수 없다.
이 경우에 인덱서블 타입을 활용하는 것이 좋다.
인덱서의 타입은 string
과 number
만 지정할 수 있다.
예를 들어 DB에 저장된 color 데이터를 조회하는 API를 호출한다고 가정해보자. 누군가 DB에 color 데이터를 추가할 때마다 추가된 color의 타입을 지정할 수는 없을 것이다.
// 이렇게 color 마다 타입을 지정할 수는 없을 것이다.
interface colorList {
'white': string;
'red': string;
.....
}
// 인덱서블 타입을 통해서 color 타입을 지정하면 모든 color를 커버할 수 있다.
interface colorList {
[key: string]: string;
}
const colorList: colorList = getColorList();