타입스크립트의 인터페이스
🐕 인터페이스
타입스크립트의 핵심 원칙 중 하나는 타입 검사가 값의 형태에 초점을 맞추고있다는 것입니다.
이것을 덕 타이핑(duck typing) 혹은 구조적 서브타이핑(Structural Subtyping)이라고 합니다.
덕 타이핑이란 개념이 개인적으로 재미있었는데
https://ko.wikipedia.org/wiki/%EB%8D%95_%ED%83%80%EC%9D%B4%ED%95%91
덕 타이핑 - 위키백과, 우리 모두의 백과사전
위키백과, 우리 모두의 백과사전. 컴퓨터 프로그래밍 분야에서 덕 타이핑(duck typing)은 동적 타이핑의 한 종류로, 객체의 변수 및 메소드의 집합이 객체의 타입을 결정하는 것을 말한다. 클래스
ko.wikipedia.org
위키피디아를 참고해보면 덕타이핑은 덕 테스트에서 유래했다고합니다.
어떤 새가 오리같이 보이는 요소들을 가지고 있다면 나는 그 새를 오리라고 부를 것이다.
개념이 재미있지 않나요?
이처럼 덕타이핑은 동적 타이핑의 한 종류로서 객체의 변수 및 메서드의 집합이 객체의 타입을 결정하는 것을 의미합니다.
즉 덕 타이핑은 객체가 어떤 타입에 걸맞는 변수와 메서드를 지니면
객체를 해당 타입에 속하는 것으로 간주한다는 것입니다.
덕 타이핑은 타입스크립트에 국한된 개념이 아니라 프로그래밍 전반에서 적용되는 개념이라는 것 기억해두고
타입스크립트에서는 어떻게 덕타이핑이 적용되는지 이제부터 보면 될 것 같네요!
👻타입스크립트로 이해하기
function printLabel(labeledObj: { label: string }) {
console.log(labeledObj.label);
}
let myObj = { size: 10, label: 'Size 10 Object' };
printLabel(myObj);
먼저 인터페이스를 사용하지 않은 간단한 예제를 만들어보겠습니다.
파라미터로 객체인데 label 속성의 밸류를 string으로 갖고 있어야 하는 함수
printLabel을 생성해주었습니다.
위 예제는 myObj의 label 속성이 string이기때문에 잘 동작합니다.
interface LabeledValue {
label: string;
}
function printLabel(labeledObj: LabeledValue) {
console.log(labeledObj.label);
}
let myObj = { size: 10, label: 'Size 10 Object' };
printLabel(myObj);
방금 전의 코드를 인터페이스를 활용하여 다시 정의해주었습니다.
LabeldValue라는 인터페이스는 label:string 이 만족되는 경우를 말합니다.
여기서 중요한 것은 형태 뿐이기 때문에 함수에 전달된 객체가
나열된 요구 조건(LabeldValue인가?)를 충족하면 허용됩니다.
또한 타입 검사는 프로퍼티의 순서를 요구하지 않습니다.
딱 인터페이스가 요구하는 프로퍼티들이 존재하는지 / 요구하는 타입을 가졌는지만 확인합니다!
🥶 선택적 프로퍼티 (Optional Properties)
그런데 인터페이스의 모든 프로퍼티가 항상 필요한 것은 아닐 수 있습니다.
특정 조건에서만 필요하거나 아예 필요 없을 수도 있습니다.
따라서 그렇게 가변적으로 필요한 경우 Optional Properties를 사용할 수 있습니다.
문법은 자바스크립트의 옵셔널 체이닝 연산자 문법과 매우 유사합니다.
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"});
선택적 프로퍼티는 위와 같이 프로퍼티 이름끝에 ?를 통해 표시합니다.
선택적 프로퍼티는 인터페이스에 속하지 않는 프로퍼티의 사용을 방지하고
사용 가능한 속성을 기술하는 것입니다.
이렇게 오타를 방지시켜줍니다.
이렇게 인터페이스에 없는 속성을 선언하려고 하는 경우에도 경고를 해줍니다.
하지만 선택적 프로퍼티는 말그대로 선택이기 때문에 width 속성이 없어도
SquareConfig 인터페이스로 성립하게 됩니다!
🌞읽기 전용 프로퍼티(Readonly Properties)
interface Point {
readonly x: number;
readonly y: number;
}
읽기 전용 프로퍼티는 readonly 키워드를 통해 지정할 수 있습니다.
이러한 읽기 전용 프로퍼티의 경우엔 처음 생성할 때만 수정이 가능하고
생성한 이후에는 수정이 불가능해집니다!
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // 오류!
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // 오류!
ro.push(5); // 오류!
ro.length = 100; // 오류!
a = ro; // 오류!
또한 타입스크립트에서는 ReadonlyArray 타입을 제공합니다.
따라서 ReadonlyArray 타입을 이용하면 배열이 변경되지 않을 것을 보장할 수 있습니다.
마치 const로 선언한 변수같이 동작하는데
const와의 차이점은 const는 push 등 배열메서드를 이용해 수정하는 것은 허용되는 반면
readonly는 배열메서드와 같은 방식으로 수정하는 것 조차도 허용되지않는다는것을 알 수 있네요!
readonly | const |
참조자료형의 수정도 허용하지않음 | 참조자료형의 수정은 허용하나 재할당은 금지 |
😋초과 프로퍼티 검사 (Excess Property Checks)
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
// ...
}
let mySquare = createSquare({ colour: "red", width: 100 });
타입스크립트는 다음과 같은 경우 버그가 있을 수 있다고 생각합니다.
객체 리터럴은 다른 변수에 할당할 때 혹은 인수로 전달할 때 특별한 처리를 받으며
초과 프로퍼티 검사를 받습니다.
만약 객체 리터럴이 대상 타입이 갖고 있지 않은 프로퍼티를 갖고있다면 에러가 발생합니다.
우리가 SquareConfig 인터페이스가 갖고있지않은 colour 프로퍼티를 넘겨주려고 하는 경우에요!
만약 이 검사를 피하고 싶다면 타입 단언을 사용하는 것을 고려할 수 있습니다.
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
// ...
}
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);
이런식으로요!
let mySquare = createSquare(<SquareConfig>{ width: 100, opacity: 0.5 });
angle bracket 방식으로 작성하고자한다면 이렇게 작성할수도있을 것입니다
하지만 특별한 경우에는 문자열 인덱스 서명(string index signatuer)를 추가하는 게
더 나은 방법일 수 있습니다.
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}
이런식으로 작성하면 SquareConfig가 여러 프로퍼티를 가질 수 있고
그 프로퍼티들이 color나 width만 아니라면 타입은 any 즉 아무거나 상관없다라는 의미가 됩니다.
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}
const obj: SquareConfig = {
color: 'gk',
width: 123,
hi: 123,
cj: '엄준식',
};
이렇게요!!
🤢함수 타입 (Function Types)
인터페이스는 자바스크립트 객체가 가질 수 있는 넓은 범위의 형태를 기술 할 수 있습니다.
따라서 자바스크립트 세계관에서는 객체이기도 한 함수 역시도 인터페이스로 타입을 설명할 수 있습니다.
인터페이스로 함수 타입을 기술하기 위해서는 인터페이스에 호출 서명(call signature)를 전달합니다.
이것은 매개변수 목록과 반환타입만 주어진 함수 선언이랑 비슷하게 느껴져요!
각 매개변수는 이름과 타입이 모두 필요합니다.
interface SearchFunc {
(source: string, subString: string): boolean;
}
이런식으로 선언할 수 있을 것입니다.
이제 덕타이핑의 논리로 생각해보면 string 타입의 매개변수 두개를 가지면서
boolean 값을 리턴하는 함수는 SearchFunc 타입으로 볼 수 있을 것입니다.
interface SearchFunc {
(source: string, subString: string): boolean;
}
const he: SearchFunc = (source: string, subString: string) => {
return true;
};
let ay: SearchFunc;
ay = function (src: string, sub: string) {
return false;
};
따라서 위와같은 형식으로 작성할 수 있습니다.
매개변수의 이름까지 같을 필요는 없고 타입만 같으면 되기 때문에
아래와 같이 매개변수의 이름이 달라져도 오류가 발생하지 않습니다.
이미 he 변수에는 SearchFunc 타입이라고 명시를 해주었기 때문에
타입스크립트는 위와 같은 상황에서 문제를 알려줍니다.
🤮인터페이스 확장(Extending Interfaces)
interface Shape {
color: string;
}
interface Square extends Shape {
sideLength: number;
}
let square = {} as Square;
square.color = "blue";
square.sideLength = 10;
마치 클래스 문법의 확장과 같이 인터페이스도 확장 문법을 사용할 수 있습니다.
확장을 할때는 부모 인터페이스의 속성을 가지게 할 수가 있네요
Shape의 내용도 가져오게되기때문에
Shape에서 요구하는 color:string 형식이 포함되지 않은 경우에
타입스크립트는 문제를 알려줍니다.
🐶마치며
입문하는 수준에서 어렵게 느껴질 수 있는 개념들은
최대한 쳐내고 바로 필요할 것 같은 기능들만 소개했습니다.
좀 더 심화적인 내용을 원하시는 경우
https://typescript-kr.github.io/pages/interfaces.html
TypeScript 한글 문서
TypeScript 한글 번역 문서입니다
typescript-kr.github.io
위 링크를 참고하시는 것을 추천드립니다.