DND 팀프로젝트를 진행하면서 타입 스크립트를 보다 제대로 사용할 수 있게 됐다. 그래서 프로젝트가 끝나가는 시점에 사용했던 타입 스크립트에 대해 제대로 정리하려고 한다. 우선 타입 별칭을 사용해서 프로젝트를 진행했다. 당시 일관성을 고려해서 type을 선정했는데 지금 생각해 보면 잘못된 선택은 아니라고 생각된다. 그 결과 타입 스크립트의 고급 기능인 매핑 기능, 선언된 타입 기반 유니온 타입 기능 등 여러 가지 기능들을 활용해서 타입 중복 선언을 막게 됐다.
타입을 선언하는데 두 가지 방법이 존재한다.
예제를 살펴보면서 이해해보려고 한다
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를 활용하여 타입을 선언해도 별반 다르지 않을 것 같을 경우에는 일관성과 보강관점에서 선정하면 된다. 우리가 돈워리 프로젝트를 하기 전, 타입 별칭을 활용해서 타입 선언을 한 이유는 결국 일관성 측면이 컸고 사용하다 보니 유니온 타입 기능을 활용해서 기존 타입을 재활용할 수 있게 됐다.