type과 interface, 그리고 Record

박희수·2024년 5월 2일
0

🙋‍♀️ 이 포스트는 '우아한 타입스크립트 with React' 도서를 읽고 적은 글입니다.

type과 interface를 사용하는 이유

타입스크립트에서 흔히 객체를 타이핑하기 위해 자주 사용하는 키워드로는 type과 interface가 있습니다.
중괄호를 사용한 객체 리터럴 방식으로 타입을 매번 일일이 지정하기에는 중복적인 요소가 많습니다. 그래서 type과 interface를 사용해 아래와 같이 선언하곤 하는데요, 이렇게 따로 타입을 빼서 선언해주면 반복적으로 사용돼도 중복 없이 해당 타입을 쓸 수 있다는 장점이 존재합니다.

type NoticePopupType = {
 title : stirng;
 description : string;
}

interface INoticePopup {
 title : string;
 description : string;
}

const noticePopup1 : NoticePopupType = { ... };
const noticePopup2 : INoticePopup = { ... };

타입스크립트에서는 일반적으로 변수 타입을 명시적으로 선언하지 않아도 컴파일러가 자동으로 타입을 추론합니다. 즉, 타입스크립트 컴파일러가 변수 사용방식과 할당된 값의 타입을 분석해 타입을 유추한다는 것을 의미합니다.

일반적 변수에서는 타입을 명시적으로 선언하지 않아도 되지만, 어떤 타입의 값이 들어올 지 모르는 경우에는 타입을 필수적으로 명시하는 게 좋을 것 같습니다.

Record 타입...? 사용해본 적이 없습니다.

🤔 type과 interface 키워드를 어떻게 사용할까?라는 질문에 대한 답 중 'props에 Record 형식을 extends를 할 때 interface로 선언된 변수를 넣으면 에러가 발생해 type으로 바꿔 넣은 경험이 있다'라는 답변을 보았습니다.

질문에 대한 모든 답은 이해가 가고 납득이 가기도 했는데 Record 타입은 들어만 봤지 직접 사용해본 적이 없기 때문에 더 자세히 찾아보았습니다.

🔺 우선 가장 기본적인 예시를 보겠습니다.

type Score = {
 [name : string] : number;
}

type ScoreRecord = Record<string, number>; 
-> 이렇게 사용하면 Score로 정의한 타입과 같은 역할을 합니다.

let scores : ScoreRecord = {
 '축구' : 100,
 '야구' : 200,
}

type Score = {
 [name : '축구' | '야구'] : number;
}
-> 이런 경우 인덱스 시그니처는 key 타입으로 문자열 리터럴을 사용할 수 없습니다. 

type Names = '축구' | '야구'; 

type Score = {
 [name in Names] : number;
}
-> 인덱스 시그니처가 문자열 리터럴 사용이 어려웠는데, 위처럼 맵드 타입을 사용해 해결이 가능합니다.

type ScroeRecord = Record<Names, number>;
📌 여기서 잠깐! 인덱스 시그니처가 무엇일까요?
인덱스 시그니처는 {[key:T]:U} 형식으로 객체가 여러 Key를 가질 수 있으며 Key와 매핑되는 Value를 가지는 경우 사용합니다. 

인덱스 시그니처는 객체가 <Key, Value> 형식이며 Key와 Value 타입을 정확하게 명시해야 하는 경우 사용합니다. 

let countResult {
 views : 12,
 likes : 100,
 follow : 32
}

function totalFunc(count : {[key : string] : number}) { 
 let total = 0;
 for (const key in count) {
  total += salary[key];
 }
 return total;
}
-> 인덱스 시그니처를 사용한 예시인데요, 이는 속성 이름 대신 대괄호 안에 Key 타입을 작성해 나타낼 수 있습니다. 

인덱스 시그니처는 string, number, symbol, Template literal 타입을 허용할 수 있습니다.

🔺 두번째 예시입니다.

interface CountType {
 views : number;
 likes : number;
 shares : number;
 comments : number;
}

interface ChartType {
 axis : string; 
 value : string;
 color : string;
}

-> 위처럼 인터페이스로 타입을 선언했다고 가정해 봅시다.
이렇게 타입을 선언한 경우, 데이터 객체 자체의 타입은 어떻게 사용할 수 있을까요?

const chartData : {
 [K in keyof CountType] : ChartType[]
} = {
 views : [...],
 likes : [...],
 shares : [...],
 comments : [...],
}

-> chartData라는 변수 자체에 맵드 타입을 사용해 넣었는데, 이걸 interface로 따로 뺀다면 반복적으로 사용할 수 있고, 좋을 것 같은데요 아쉽게도 저 타입만 따로 빼서 인터페이스를 정의하는 건 불가능합니다.

만약 interface로 빼고 싶다면, 인터페이스 안에 속성을 따로 사용할 수 있습니다.
아래의 코드를 보겠습니다.

interface ChartDataType {
 data : {
  [K in keyof CountType] : ChartType[];
 }
}

-> 이렇게 인터페이스 안에 data라는 속성을 따로 지정해서 선언했습니다. 그리고 아래와 같이 사용할 수 있습니다.

const chartData : ChartDataType = {
 data : {
  views : [], 
  likes : [],
  shares : [],
  comments : []
 }
}

const chartData2 : ChartDataType['data'] = {
 views : [],
 likes : [],
 shares : [],
 comments : [],
}

->data 속성으로 따로 감싸주거나, 타입을 적용할 때 ['data']로 접근할 수 있습니다.

⭐ 그리고 가장 간단한 방법은 interface가 아닌 type으로 따로 선언해주는 것입니다.

type ChartDataType = {
 [K in keyof CountType] : ChartType[];
}

type으로 선언하는 것도 좋지만 이 때, Record 타입을 사용하면 더 간단하게 정의할 수 있습니다.

type ChartDataType = Record<keyof CountType, ChartType[]>;

const chartData : ChartDataType = {
 views : [],
 likes : [],
 shares : [],
 comments : [],
}

-> 이전의 맵드 타입보다 코드가 조금 더 간소화 된 것 같죠? 코드뿐만 아니라 맵드 타입을 이용해 type으로 선언한 것과 다른 한가지 차이점이 또 있습니다.

interface CountType {
 views?: number;
 likes?: number;
 shares?: number;
 comments?: number;
}

const chartDataMapped : {
 [K i keyof CountType] : ChartType[]
} = {
 views : [],
 likes : [],
 shares : [], 
 comments : []
};

const chartDataRecord : Record<keyof CountType, ChartType[]> = {
 views : [],
 likes : [],
 shares : [],
 comments : [],
};

const result1 = chartDataMapped.views.find((chartData) => (...)); // error - 'chartDataByType.views' is possibly 'undefined'

const result2 = chartDataRecord.views.find((chartData) => (...)); // ok

-> 위의 예시를 보면, chartDataMapped는 맵드 타입을 적용해 CountType의 각 키의 옵셔널 정보가 포함되어 접근시 possibly 'undefined' 에러가 뜨지만 chartDataRecrod는 Record 타입을 적용해 옵셔널 정보가 포함되지 않고 다 존재하는 키로 간주해 에러를 표시하지 않습니다.

이게 또 Record 타입을 사용하는 이유가 될 수 있겠네요😉

computed value 🌼

마지막으로, 자주 보는 단어이지만 항상 헷갈렸던 computed value에 대해 바로 잡고 가겠습니다.

computed property name은 표현식을 이용해 객체의 key 값을 정의하는 문법입니다.

type names = 'firstName' | 'lastName';

type NameType = {
 [key in names] : string;
}

const result : NameTypes = {firstName : 'heeppy', lastName : 'ea'};

interface NameInterface {
 [key in names] : string; // error
}

-> 이렇게 표현식을 사용해 key 값을 정의해 interface를 선언하려고 하면 에러가 뜨는 것을 알 수 있습니다.
그렇기에 computed value를 사용해 타입을 정의할 때는 interface가 아닌, type을 사용해야 한다는 점을 명심해야 합니다!

profile
프론트엔드 개발자입니다 :)

0개의 댓글