[TS] 타입과 인터페이스 정리

이주영·2024년 3월 8일
0

typescript

목록 보기
4/4
post-thumbnail

서론

DND 팀프로젝트를 진행하면서 타입 스크립트를 보다 제대로 사용할 수 있게 됐다. 그래서 프로젝트가 끝나가는 시점에 사용했던 타입 스크립트에 대해 제대로 정리하려고 한다. 우선 타입 별칭을 사용해서 프로젝트를 진행했다. 당시 일관성을 고려해서 type을 선정했는데 지금 생각해 보면 잘못된 선택은 아니라고 생각된다. 그 결과 타입 스크립트의 고급 기능인 매핑 기능, 선언된 타입 기반 유니온 타입 기능 등 여러 가지 기능들을 활용해서 타입 중복 선언을 막게 됐다.

본론

타입을 선언하는데 두 가지 방법이 존재한다.

  1. type 선언
  2. interface 선언

공통점

예제를 살펴보면서 이해해보려고 한다

1. 잉여 타입 속성 체크해준다.

잉여 속성 체크란 말 그대로 객체 리터럴이나 다른 객체로부터 속성을 가져올 때, 해당 속성이 타입에 명시되지 않았을 때 발생하는 현상을 말한다.

type TProps = {
	name : string;
	capital : string;
}

interface IProps {
	name : string;
	capital : string;
}

const user: TProps = {
	name : 'juyoung',
	capital : 'seoul',
	age : 88; // 이경우 TProps에 age가 없다고 에러가 뜹니다. 
}

const user: IProps = {
	name : 'juyoung',
	capital : 'seoul',
	age : 88; // 이경우 TProps에 age가 없다고 에러가 뜹니다.
}


2. 인덱스 시그니처 사용 가능하다.

인덱스 시그니처란 객체의 속성에 동적으로 접근할 수 있는 방법을 제공하는 기능이다. 아마 자바스크립트만을 사용하다 타입스크립트로 시도하는 도중 가장 답답했던 부분이 아니었나 싶다.

한 가지 프로젝트 예시를 가지고 와서 설명해보려고 한다.

// svg 아이콘 맵 객체를 만들었다. 
export const iconMap = {
  filledHeart: FilledHeart,
  heart: Heart,
  kakaotalk: Kakaotalk,
};

export type IconType = keyof typeof iconMap; // iconMap 객체의 key값을 유니온으로 변경하는 코드이다. 

//iconMap 객체를 사용하는 컴포넌트 
export type Props = {
  icon: IconType;
} 

const Icon = ({ icon}: Props) => {
  const IconSVGComponent = iconMap[icon]; // 이렇게 객체 타입에 icon으로 접근할 수 있는데 이 부분은 위에서 iconMap 객체의 키값을 유니온 타입으로 변경했기에 가능해진 것. 

하지만 일반적으로 인덱스 시그니처란 아래와 같다.

type TDictionary = { [key : string] : string};
interface IDictionary {
	[key : string] : string;
}

3. 함수 타입도 지정 가능하다.

type TFunction = (x : number) => string
interface TFunction {
	(x : number) => string
}

4. 모두 제네릭이 가능하다.

type TPair<T> = {
	first: T;
	second : T;
}

interface IPair<T> {
	first: T;
	second : T;
}

제네릭에 대해서는 다른 포스트에서 정리해보려고 한다.

즉 타입 선언의 두종류 모두 잉여 타입 속성 체크해주고 인덱스 시그니처를 사용할 수 있으며 함수 타입도 지정 가능하고 마지막으로 제네릭을 사용할 수 있다는 것.

차이점

1. 인터페이스는 복잡한 타입을 확장하지 못한다.

복잡한 타입을 확장하고 싶다면 타입과 & 연산자를 사용해야한다.


type TProps = {
	name : string;
	capital : string;
}

interface IProps {
	name : string;
	capital : string;
}

interface = IPropsWithPop extends TProps {
	population : number;
}

type TStateWithPop = IProps & {population : number;};

즉 유니언 타입은 있는데 유니언 인터페이스라는 개념은 없는 것을 알 수 있다.

type Union = "A" | "B"

인터페이스는 타입을 extends 키워드로 확장할 수 있지만 유니온 인터페이스라는 개념은 없기에 할 수가 없다. 하지만 가끔씩 유니온 타입 확장이 필요한 경우도 있는데!

type Input = {
  inputProp: string;
};

interface Output {
  outputProp: number;
}

// 인터페이스로 구현한 경우
interface VariableMap {
  [name: string]: Input | Output;
}

const variableMapExample: VariableMap = {
  variable1: { inputProp: "value1" },
  variable2: { outputProp: 42 },
  variable3: { inputProp: "value2", outputProp: 3 }, // 유효하지만 일반적으로 권장되지 않는다.
};

// 타입으로 구현한 경우
type NamedVariable = (Input | Output) & { name: string };

const namedVariableExample1: NamedVariable = { name: "variable1", inputProp: "value1" };
const namedVariableExample2: NamedVariable = { name: "variable2", outputProp: 42 };
const namedVariableExample3: NamedVariable = { name: "variable3", inputProp: "value2", outputProp: 3 }; // 유효하지만 일반적으로 권장되지 않는다.

중요 ) 타입 별칭으로 선언할 경우, 유니온 타입으로 사용 될 수도 있고 매핑된 타입(이부분은 아직 정확히 모른다.)과 조건부 타입 같은 고급 기능을 활용할 수 있다.

2. 타입에 없는 인터페이스의 기능이 있는데 보강이 강하다. (즉 오픈되어 있다는 의미로 이해하고 있다.)

interface IProps {
	name : string;
	capital : string;
}

interface IProps {
	age : number;
}

const user : IProps = {
	name : 'juyoung',
	capital : 'seoul',
	age : 88; 
} // 정상적으로 작동한다. 

위와 같이 속성을 확장하는 것을 선언 병합이라고 한다.

결론

복잡한 타입은 type 별칭으로 관리하는 게 효율적이다. 만약 type과 interface를 활용하여 타입을 선언해도 별반 다르지 않을 것 같을 경우에는 일관성과 보강관점에서 선정하면 된다. 우리가 돈워리 프로젝트를 하기 전, 타입 별칭을 활용해서 타입 선언을 한 이유는 결국 일관성 측면이 컸고 사용하다 보니 유니온 타입 기능을 활용해서 기존 타입을 재활용할 수 있게 됐다.

profile
https://danny-blog.vercel.app/ 문제 해결 과정을 정리하는 블로그입니다.

0개의 댓글