interface란 TypeScript에서 객체가 어떤 형태를 가져야 하는지 정의하는 방법입니다. 각 개체마다 어떤 타입의 값을 받아올지 정할 수 있는 기능으로, 짧게 예시 코드를 짜보았습니다.
interface User {
name: string;
age: number;
isFrontend: boolean
}
위와 같이 작성하여 name의 값을 string, age의 값은 number, isFrontend의 값은 boolean 형태로 받을 수 있습니다.
코드를 작성하면서 user1, user2 등 많은 객체를 만들어야 할 때 유용하게 쓸 수 있습니다.
const user1 = {
name: "민준",
age: 17,
isFrontend: true
}
const user2 = {
name: "대은",
age: 17,
isFrontend: false
}
const user3 = {
name: "동혁",
age: 17,
isFrontend: true
}
이처럼 interface 기능을 사용하지 않는다면 각 객체마다 어떤 형태의 객체를 만들어야하는지 확인해야하는 불편함이 생기게 됩니다.
이번에는 위의 코드에 interface 기능을 도입해보겠습니다.
interface User {
name: string;
age: number;
isFrontend: boolean;
}
const user1: User = {
name: "민준",
age: 17,
isFrontend: true
};
const user2: User = {
name: "대은",
age: 17,
isFrontend: false
}
const user3: User = {
name: "동혁",
age: 17,
isFrontend: true
}
이렇게 user1, user2, user3의 객체를 구성시킬 수 있습니다. 그렇다면 interface를 대입시킨 객체에서 만약 형식을 따르지 않는다면 어떻게 될까요?
interface User {
name: string;
age: number;
isFrontend: boolean;
}
const user1: User = {
name: "민준",
age: 17,
isFrontend: "true"
};
와 같이 isFrontend의 값을 문자열로 받는다면 interface에서 정한 boolean 형식을 따르지 않기 때문에 오류가 발생하게 됩니다.
또한 마찬가지로, 만약 user1이라는 객체 안에 name, age, isFrontend에 대한 값들 중 하나라도 받지 못한다면 오류가 발생하게 됩니다.
interface를 만들 때 선택적으로 값을 따르게 만들 수 있습니다.
interface User {
name: string;
age: number;
isFrontend?: boolean;
}
const user1: User = {
name: "민준",
age: 17
}
와 같이 isFrontend라는 속성 이름 맨 끝에?를 작성함으로써 선택적으로 따를 수 있게끔 작성하는 방법도 있습니다.
하지만 모든 타입의 값이 들어가도 된다는 의미는 아닙니다. 예를 들어 저 코드에서 만약 isFrontend: "Yes"와 같이 받는다면 이는 boolean 형태가 아니기 때문에 오류가 일어나게 됩니다.
interface는 한가지 타입만 받는게 아니라, 여러 타입을 받을 수 있습니다.
interface User {
ID: string;
password: string | number;
}
const user1: User = {
ID: "10mxun",
password: 1234
};
const user2: User = {
ID: "10mxunn",
password: "qlalfqjsgh"
};
이와 같이 여러 타입을 받을 때에는 string | number처럼 타입1 | 타입2로 받을 수 있습니다. 이를 Union Type이라고 합니다.
TypeScript에서 쓰이는 Type과 interface는 서로 비슷한 듯 다릅니다. 두 개념 모두 타입을 정의하는 데 사용할 수 있다는 점에서 공통점이 있습니다.
interface User {
name: string;
age: number;
}
type User = {
name: string;
age: number;
}
모두 name은 문자열, age는 숫자 형식을 갖게끔 만들 수 있습니다.
둘의 대표적인 차이는 확장하는 방법에 있는데, interface는 extends를 사용하고, type은 &를 사용합니다. 또 다른 차이점으로, interface는 같은 이름으로 다시 선언했을 때 기존 내용이 합쳐질 수 있습니다. 하지만 type은 같은 이름으로 다시 선언할 수 없는데, 이를 선언 병합 이라고 합니다.
정리해보자면, interface와 type 둘 다 타입을 정의할 수 있지만, 확장 방법 및 선언 병합에서 차이점이 존재합니다.
위에서 type과 interface의 차이점 중 extend가 대표적이라고 설명했는데, 이를 좀 더 자세히 알아보겠습니다. extend란 기존 interface를 가져와 거기에 새로운 내용을 추가하는 것 입니다.
예를 들어 User가 있다고 해보겠습니다.
interface User {
name: string;
age: number;
}
여기에 전공이 프론트엔드인지를 알 수 있는 isFrontend 속성을 추가하고 싶다면, 기존 내용을 다시 작성하지 않고
interface Frontend extends User {
isFrontend: boolean;
}
와 같이 작성할 수 있습니다. 그러면 Frontend는 User에 있던 name, age 속성을 그대로 가져오면서 isFrontend 속성까지 가질 수 있게 됩니다.
정리하자면, extend는 기존 interface의 내용을 가져와 새로운 interface를 만들 떄 사용하는 기능인 것 입니다.
두번 째 차이점으로 설명한 선언 병합에 대해서 알아보겠습니다. interface에서 또다른 특징으로도 선언 병합을 할 수 있습니다.
예를 들어,
interface User {
name: string;
}
interface User {
age: number;
}
처럼 User이라는 이름을 두번 작성한다면 각 인터페이스가 합쳐져서
interface User {
name: string;
age: number;
}
와 같은 형태가 됩니다. 반면, type은 같은 이름으로 다시 선언할 수 없어서 이러한 선언 병합을 사용할 수 없습니다.
정리하자면, interface는 같은 이름으로 선언했을때 내용이 합쳐질 수 있지만, type은 같은 이름으로 다시 선언할 수 없다는 차이점이 있습니다.
interface의 대표적인 사용 예시로 HG 커밋 중,'feat: 권한 요청 사용자 필터 기능 추가'에서 코드를 가져와봤습니다.
export interface OneseoType {
memberId: number;
submitCode: string | null;
realOneseoArrivedYn: YesNo;
entranceIntentionYn: YesNo;
name: string | null;
screening: ScreeningType;
schoolName: string | null;
phoneNumber: string;
guardianPhoneNumber: string;
schoolTeacherPhoneNumber: string;
firstTestPassYn: YesNo | null;
competencyEvaluationScore: number | null;
interviewScore: number | null;
secondTestPassYn: YesNo | null;
examinationNumber: number | null;
oneseoEditStatus: OneseoEditStatusType;
}
와 같이 작성되어 있습니다. 대표적으로 여기에서 memberId: number;로 작성해 memberId값을 숫자로 받게끔 만들었다는 것을 알 수 있습니다. 또한, name: string | null을 통해 이름을 문자열 또는 null값으로 받는다는 것을 알 수 있습니다.
TypeScript에서 Utility Type은 기존에 정의된 타입을 기반으로 새로운 타입을 생성할 수 있도록 도와주는 내장 타입 도구 입니다.
Utility Type을 사용하면 기존에 타입을 직접 수정하거나 새로운 타입을 반복해서 선언하지 않아도 되게끔 만들어줍니다. 또한, 특정한 목적에 맞춰서 타입 구조를 변환할 수도 있습니다.
예를 들어 User라는 인터페이스를 정의되어 있다고 해보겠습니다. User 인터페이스 안에는 name, age, email이라는 세가지 속성이 있습니다.
예를 들어,
interface User {
name: string;
age: number;
email: string;
}
const updateUser = {
name:"민준"
};
이 코드를 User 타입으로 지정하면 age와 email값이 없기 때문에 오류가 발생합니다.
그래서 직접 새로운 타입을 만들 수도 있습니다.
interface UpdateUser {
name?: string;
age?: number;
email?: string;
}
이렇게 작성할 순 있지만, 기존에 있던 User 인터페이스와 비슷한 내용을 다시 작성해야 한다는 문제가 있습니다.
이 문제를 해결하기 위해 Utility Type을 사용합니다.
type UpdateUser = Partial<User>;
이렇게 사용할 수 있습니다. Partial<User>을 사용해 User에 정의된 모든 속성을 선택적으로 변경한 새로운 타입을 생성할 수 있습니다.
방금 전에User을 만들었는데, 여기에
type UpdateUser = Partial<User>;
을 사용해서 User의 모든 속성을 선택적 속성으로 바꿀 수 있습니다. 이를 쉽게 풀어서 설명하자면,
type UpdateUser = {
name?: string;
age?: number;
email?: string;
}
과 같이 타입을 만들어준다고 할 수 있습니다.
앞서 설명한 개념인 ?와 비슷한 점이 많지만, Partial을 꼭 써야하는 이유가 있습니다. 우선 ?의 경우 특정 속성을 선택적으로 지정할 수 있다는 장점이 존재하지만, 모든 속성에 적용해야하는 경우 번거로워질 수 있습니다.
이를 해결하기 위해서 모든 속성을 선택적 속성으로 변경해주는 Partial을 사용한다면 훨씬 코드를 작성하는 데에 편리해지게 됩니다.
interface User {
name: string;
age: number;
email: string;
password: number | string;
birthday_year: number;
birthday_month: number;
birthday_day: number;
phone_number: number;
}
type UpdateUser = {
name?: string;
age?: number;
email?: string;
password?: number | string;
birthday_year?: number;
birthday_month?: number;
birthday_day?: number;
phone_number?: number;
}
와 같이 만약 많은 속성들에 하나하나 ?를 달게 된다면 코드 작성에 불편함을 느낄 수 있는데,
interface User {
name: string;
age: number;
email: string;
password: number | string;
birthday_year: number;
birthday_month: number;
birthday_day: number;
phone_number: number;
}
type UserUpdate = Partial<User>;
이렇게 코드의 양을 줄일 수 있게 되어 가독성 또한 좋아지게 됩니다. 즉, Partial의 핵심은 기존 타입의 모든 속성을 선택적 속성으로 바꿔준다는 것입니다.
앞서 설명한 Partial은 모든 속성을 선택적으로 바꿨다면, Required는 반대로 모든 속성을 필수적으로 바꿉니다.
예를 들어서,
interface User {
name?: string;
age?: number;
isFrontend?: boolean;
}
과 같이 작성되어 있다면, 모든 속성에서 하나하나 ?를 지울 필요 없이
type UpdateUser = Required<User>;
과 같이 작성할 수 있습니다.
Readonly는 이름에서 알 수 있듯이, 기존 타입의 모든 속성을 읽기 전용으로만듭니다.
예를 들어,
interface User{
name: string;
age: number;
}
const user: User = {
name: "민준",
age: 17
};
user.name = "최민준";
user.age = 18;
이렇게 User이 있다고 가정해보겠습니다. 일반적인 interface였다면 객체 생성 후 값을 변경할 수 있습니다. 이를 Readonly를 통해 읽기 전용으로 바꿔서 값을 변경할 수 없게 만들 수 있습니다.
interface User{
name: string;
age: number;
}
type ReadonlyUser = Readonly<User>
로 바꿀 수 있습니다. 이를 풀어서 작성하게 된다면,
type ReadonlyUser = {
readonly name: string;
readonly age: number;
}
와 같습니다.
추가로 Readonly는 값을 못읽게 하는 것이 아닌 수정을 못하게 하는 것입니다.
Pick 또한 말 그대로 기존 타입에서 원하는 속성만 골라 새로운 타입을 만드는 것입니다.
interface User{
name: string;
age: number;
email: string;
}
여기에서 name과 email만 필요한 상황이라고 가정해보겠습니다.
type UserInfo = Pick<User, "name" | "email">;
와 같이 쓸 수 있습니다. 이렇게 작성한 코드를 풀어서 써보면
interface User {
name: string;
email: string;
}
와 같은 의미를 가지게 됩니다.
Omit은 Pick과 반대로, 특정 속성을 제외하고 나머지를 가져옵니다.
type UserInfo = Omit<User,"email">;
와 같이 쓰게된다면, "email속성 빼고 나머지 속성만 고른다"라는 의미로 볼 수 있습니다. 이 또한 풀어서 써보면,
interface User {
name: string;
age: number;
}
와 같이 써볼 수 있습니다.
Pick과 Omit 둘 다 특정 속성을 고른다는 큰 틀은 같지만, Pick은 특정 속성을 직접 고르는 것이고, Omit은 제외할 속성을 고른다는 점에서 차이점이 있습니다.
Record는 특정 키와 값의 타입을 정해 객체의 타입을 만드는 것 입니다.
만약,
type UserRole = Record<string, string>;
이렇게 코드를 짠다면 문자열 키를 가지고 문자열 값을 갖는 객체를 만들 수 있습니다.
const userRole: UserRole = {
admin: "admin",
user1: "user",
user2: "user"
}
이 코드와 비교해보면 이해하기 쉽습니다.
Record<string, string>과 같이 작성했는데, 첫번째 string은 키, 두번째 string은 값으로 볼 수 있습니다.
키의 경우, 특정 키를 정할 수도 있습니다.
const role: Role = {
admin: "관리자",
user: "사용자",
guest: "게스트"
};
처럼 사용하룻도 있습니다.
Record<K,T>는 지정된 K에 대해서 값의 타입인 T를 가지는 객체 타입을 생성하게 됩니다.
Exclude는 위에서 설명한 Union Type과 연결됩니다.
type Status = "loading" | "success" | "error";
만약 여기에서 error만 제외하고 싶다면,
type AvailableStatus = Exclude<Status, "error">;
와 같이 작성할 수 있습니다. 즉, Exclude는 Union Type에서 특정 타입을 제거하는 개념이라 할 수 있습니다
Extract는 Exclude의 반대 개념으로, 특정 타입만 가져올 수 있습니다.
type Status = "loading" | "success" | "error";
여기서 success라는 타입을 가져오려면
type SuccessStatus = Extract<Status,"success">;
와 같이 작성하면 됩니다.
앞서 설명했던 Pick과 Omit의 차이점과 비슷합니다. Exclude와 Extract 모두 Union Type에서 특정 타입을 고를 수 있습니다. 하지만 Exclude는 그 타입을 제외하고, Extract는 특정 타입을 가져올 수 있다는 차이점이 있습니다.
이것도 이름 그대로 해석해보면 의미를 유추 해볼 수 있다. null과 undefined를 제거하는 Utility Type이다.
type UserName = string | null | undefined;
이렇게 간단하게 만들어보았다. 여기에서,
type ValidUserName = NonNullable<UserName>;
이렇게 쓰게 된다면 결과는
type ValidUserName = string;
으로 나오게 된다. 즉, NonNullable을 통해 string, null, undefined중에서 null과 undefined를 제거해 string만 남게 된 것이다.
이제 함수와 관련된 ReturnType에 대해서 알아보겠습니다.
함수 getUser가 있다고 해보겠습니다.
function getUser() {
return {
name: "민준",
age: 17
};
}
이 함수에서 반환하는 값의 타입을 직접 작성하지 않고 가져오고 싶다면,
type User = ReturnType<typeof getUser>;
와 같이 쓸 수 있습니다. 그러면 User는
type User = {
name: string;
age: number;
};
처럼 만들어지게 됩니다. 즉, 함수가 반환하는 값의 타입을 가지게 되는것 입니다.
ReturnType에서 반환값을 가져왔다면 Parameters는 매개변수를 가져옵니다. 이번엔 예시로 login이라는 함수를 만들어보겠습니다.
function login(id: string, password: string) {
if (id === "admin" && password === "1234") {
return "로그인 성공"
}
return "로그인 실패";
}
이 함수에서,
type LoginParams = Parameters<typeof login>;
처럼 쓰게 된다면
type LoginParams = [id: string, password: string];
와 같이 함수의 매개변수 타입을 튜플 형태로 가져오게 됩니다.
Return Type과 Parameter는 둘다 함수에서 특정 값의 타입을 가지게 된다는 공통점이 있습니다. 하지만 Return Type은 함수의 반환값을, Parameters는 함수의 매개변수의 타입을 가지게 된다는 차이점이 존재합니다.
| Utility Type | 역할 |
|---|---|
Partial | 모든 속성을 선택적으로 변경 |
Required | 모든 속성을 필수로 변경 |
Readonly | 모든 속성을 읽기 전용으로 변경 |
Pick | 원하는 속성만 선택 |
Omit | 원하는 속성을 제외 |
Record | 특정 키와 값 타입을 가진 객체 생성 |
Exclude | Union에서 특정 타입 제외 |
Extract | Union에서 특정 타입 추출 |
NonNullable | null, undefined 제거 |
ReturnType | 함수의 반환 타입 추출 |
Parameters | 함수의 매개변수 타입 추출 |
Utility Type은 기존에 정의된 타입을 그대로 다시 작성하지 않고, 필요한 목적에 맞춰 변형해 새로운 타입을 만들 수 있도록 도와주는 내장 타입 도구 입니다.
Partial, Required, Readonly를 통해 기존 타입의 속성을 각각 선택적, 필수적, 읽기 전용으로 변경할 수 있습니다. Pick과 Omit을 통해 기존 타입에서 필요한 속성만 선택하거나 특정속성을 제외할 수 도 있습니다.
또한, Record를 통해 특정한 키와 값의 타입을 가진 객체를 만들 수 있으며, Exclude와 Extract, NonNullable을 통해 Union Type에 포함된 타입을 원하는 형태로 변경할 수 있습니다.
함수와 관련된 Utility Type인 ReturnType과 Parameters는 각각 함수의 반환타입과 매개변수 타입을 가져올 수 있었습니다.
Utility Type은 기존 타입을 다시 작성하는 대신, 이미 만든 타입들을 재사용하고 필요한 형태로 변환할 수 있게 해주는 기능 입니다. 이를 통해서 중복되는 타입 선언을 줄일 수 있고, 코드의 가독성과 유지 보수성 또한 높일 수 있습니다.
각 Utility Type들은 다른 목적을 가지고 있지만, 기존 타입을 기반으로 해서 새로운 타입을 만들어 TypeScript의 타입을 더 효율적으로 관리하기 위해 사용한다는 공통점을 가지고 있습니다.