코딩 마을 방범대 : 3장. 고급 타입
질문과 답변
1. 불필요한 제네릭 사용
제네릭을 굳이 사용하지 않아도 되는 코드를 적절하게 수정해보세요.
타입스크립트에서 제네릭이 필요하지 않을 때에도 무분별하게 사용할 경우 코드의 길이만 늘어나고 가독성을 해칠 수 있습니다.
아래의 코드에서 불필요한 부분을 찾아내 코드를 개선해보세요.
type Identity<T> = T;
type Status = "ACTIVE" | "INACTIVE" | "PENDING";
interface User {
getStatus: () => Identity<Status>;
}
const user: User = {
getStatus: () => "ACTIVE"
};
lulla-by
type Status = "ACTIVE" | "INACTIVE" | "PENDING";
interface User {
getStatus: () => Status;
}
const user: User = {
getStatus: () => "ACTIVE"
};
eeeyooon
Identity<T>
는 T
를 그대로 반환하기만 하고 있어 제네릭이 불필요합니다. Identity<T>
를 제거하고, getStatus
가 직접 Status
를 반환하도록 수정할 수 있습니다.
type Status = "ACTIVE" | "INACTIVE" | "PENDING";
interface User {
getStatus: () => Status;
}
const user: User = {
getStatus: () => "ACTIVE"
};
sryung1225
Identity
제네릭은 타입을 그대로 반환하는 역할을 하고 있습니다.Identity
가 사용되고 있는 getStatus
은 단순히 Status
타입의 값을 반환하는 것이기 때문에 굳이 Identity
를 사용하는 의미가 없어 제거 가능합니다.아래와 같이 코드를 개선시킬 수 있습니다.
type Status = "ACTIVE" | "INACTIVE" | "PENDING";
interface User {
getStatus: () => Status;
}
const user: User = {
getStatus: () => "ACTIVE"
};
출제자 : Stilllee
이 코드에서
Identity
라는 타입은 단순히 제네릭 매개변수T
를 그대로 반환하고 있습니다.
User
인터페이스의getStatus
메서드에서Identity<Status>
를 반환타입으로 사용할 필요없이 직접Status
의 타입을 사용하는것이 더 간결하고 명확합니다.따라서 다음과 같이 코드를 수정할 수 있습니다.
type Status = "ACTIVE" | "INACTIVE" | "PENDING"; interface User { getStatus: () => Status; } const user: User = { getStatus: () => "ACTIVE" };
이렇게 수정하면 코드가 더 이해하기 쉽고 간결해지며,
Status
타입의 목적과 사용이 명확해지므로 협업 시에도 실수를 줄일 수 있습니다.
2. enum의 특징
enum의 특징인 직접 할당되지 않은 멤버의 값을 추론하는 방식이 안전하지 이유는 무엇인가요?
enum 타입은 타입스크립트에서 지원하는 특수한 타입입니다. 각 멤버에 명시적으로 값을 할당할 수도 있고, 아래와 같이 값을 스스로 추론하도록 할 수도 있습니다.
enum HttpStatusType {
Forbidden = 403,
NotFound,
BadGateway = 502,
}
console.log(HttpStatusType[404]); // ? "NotFound" // 역방향 접근
하지만 이런 타입스크립트가 자동으로 추론한 enum은 안전하지 않다는 이유로 책에서 권장하지 않고 있습니다. 안전하지 않다고 평가한 근거는 무엇인가요? 그리고 대신에 어떤 방식으로 선언된 enum을 권장하는 지 설명하시오.
Stilllee
1. 값의 중복과 예상치 못한 결과
타입스크립트에서
enum
을 사용할 때, 명시적으로 값을 할당하지 않으면 이전 멤버의 값을 기반으로 다음 멤버의 값을 자동으로 할당하게 됩니다.
enum HttpStatusType {
Forbidden = 403,
NotFound, // 자동으로 404가 할당됨
BadGateway = 502,
}
이는 명시적으로 선언되지 않았기 때문에 코드를 읽는 사람이나 컴파일러가 예측하기 어렵습니다.2. 명시적 재할당으로 인한 중복 값
enum
내에서 값이 명시적으로 재할당되면, 다른 멤버가 같은 값을 갖게 되어 예상치 못한 중복 값이 발생할 수 있습니다.
enum HttpStatusType {
Forbidden = 403,
NotFound, // 자동으로 404가 할당됨
BadGateway = 404, // BadGateway에 404라는 값을 명시적으로 재할당
}
예를들어 위 코드의 경우 NotFound
와 BadGateway
두 멤버 모두 같은 값인 404
를 갖게 됩니다. 이는
enum
의 기본 목적인 각 멤버의 고유성을 해치며, 런타임에 예상치 못한 결과를 초래할 수 있습니다.3. 역방향 접근의 문제
enum
은 역방향으로도 접근할 수 있습니다. 즉, 숫자를 사용하여 해당 숫자에 할당된 enum
멤버의 이름을 얻을 수 있습니다. 이는 할당된 값의 범위를 넘어서는 접근을 시도할 때도 막지 않으므로, 예상치 못한 동작을 유발할 수 있습니다.
// 정상적인 역방향 접근
console.log(HttpStatusType[404]); // "NotFound"
// 범위를 넘어서는 역방향 접근
console.log(HttpStatusType[405]); // undefined 또는 예상치 못한 결과
대신 권장되는 방식
1. const enum 사용
const enum
은 역방향으로의 접근을 허용하지 않으며, 컴파일 시 enum
을 인라인 상수로 변환하여 성능상의 이점을 제공합니다.이는 자바스크립트에서 객체에 접근하는 것과 유사한 동작을 보장하며, 불필요한 런타임 오버헤드를 줄여줍니다.
2. 명시적 값 할당
모든
enum
멤버에 대해 명시적으로 값을 할당하면 자동 값 추론으로 인한 혼란을 방지할 수 있으며, 코드의 가독성과 예측 가능성이 높아집니다.
lulla-by
const enum으로 열거형을 선언하여 역방향 접근을 허용하지 않는 방법을 권장하며 숫자 상수 방식은 선언하지 않은 멤버로 접근 및 할당이 가능하기에 이를 막아주는 문자열 상수 방식으로 선언하는 것을 권장합니다.
eeeyooon
이러한 enum의 문제점 때문에 책에서는
const enum
으로 열거형을 선언하는 방법을 권장합니다. 다만 const enum
으로 열거형을 선언하더라도 숫자 상수로 관리되는 열거형은 선언한 값 이외의 값을 할당하거나 접근할 때 이를 방지하지 못합니다. 반면 문자열 상수 방식으로 선언한 열거형은 미리 선언하지 않은 멤버로 접근을 방지하기 때문에 숫자 상수 방식보다 더 안전하며 의도하지 않은 값의 할당이나 접근을 방지하는 데 도움이 되는 문자열 상수 방식을 권장합니다.
출제자 : sryung1225
☑ 예시 코드와 같이 enum은 직접 할당한 값 또는 추론 된 값을 이용해 멤버를 알아내는 역방향 접근이 가능합니다. 문제는 할당한 값을 넘어서는 범위로도 역방향 접근이 가능하다는 것입니다.
console.log(HttpStatusType[404]); // ? "NotFound" console.log(HttpStatusType[476]); // ? undefined
위에서 자동으로 추론된
NotFound = 404
와BadGateway = 502
사이 임의의 값으로 역방향 접근을 하게 될 경우undefined
를 출력해 냅니다.여기에서 핵심은 실행 과정에서 막히는 부분이 없다는 사실입니다. 에러가 발생하지 않는 이 결과를 안전하지 않은 결과라고 평가합니다.
때문에 더 안전한 방식은 상수 enum을 사용하는 것입니다.
const enum
을 이용해 enum을 선언하는 경우 역방향 접근을 아예 허용하지 않게 됩니다.
const enum HttpStatusType { Forbidden = 403, NotFound, BadGateway = 502, } console.log(HttpStatusType[404]); // 🚨 A const enum member can only be accessed using a string literal.
3. any 타입과 제네릭 타입
any 타입과 제네릭 타입을 사용하여 배열을 생성했을 때의 차이점을 설명해주세요.
107쪽에서
앞서 제네릭이 일반화된 데이터 타입을 말한다고 했는데, 이 표현만 보면 any의 쓰임과 혼동할 수도 있을 것이다. 하지만 둘은 명확히 다르다. 둘의 차이는 배열을 떠올리면 쉽게 알 수 있다.
라고 서술하고 있는데, 둘의 차이점이 무엇이 있는 지 설명해주세요.
Stilllee
any
타입의 배열에는 모든 타입의 요소가 들어갈 수 있습니다. 타입 검사를 하지 않고 모든 타입이 허용되는 타입으로 취급되기 때문에 요소들의 타입이 전부 같지 않을 수 있습니다.제네릭은 배열 생성 시점에 원하는 타입으로 특정할 수 있어 배열 요소가 전부 동일한 타입이라고 보장할 수 있으며 타입 검사가 이루어지기 때문에 타입 오류를 사전에 방지할 수 있습니다.
lulla-by
sryung1225
// any 타입의 배열 : 배열 요소들의 타입이 서로 다를 수 있음
const arrayAny: any[] = [1, "two", true];
// generic 타입의 배열 : 배열 요소들이 특정된 타입으로 선언되어 동일함
const arrayGeneric: Array = [1, "two", true];
// 🚨 Type 'string' is not assignable to type 'number'.
// 🚨 Type 'boolean' is not assignable to type 'number'.
출제자 : eeeyooon
any
타입의 배열은 배열의 각 요소가 어떤 타입이든 될 수 있습니다. 어떤 타입의 데이터도 저장될 수 있지만, 타입 검사를 하지않고 모든 타입이 허용되는 타입으로 취급되기 때문에 타입 안정성이 떨어지고 런타임에 타입 관련 오류가 발생할 가능성이 높아집니다.let arrayAny: any[] = [1, "string", true, { key: "value" }];
제네릭 타입의 배열은 배열 생성 시점에 원하는 타입을 지정하며, 선언 시 지정된 타입의 요소만 포함할 수 있습니다. 그렇기 때문에 배열의 모든 요소가 동일한 타입임을 보장합니다. 이는 타입 안정성을 높이고, 오류 가능성을 줄입니다.
let arrayGeneric: Array<number> = [1, 2, 3, 4]; arrayGeneric = [1, "string", true]; // error: Type 'string' is not assignable to type 'number' // Type 'boolean' is not assignable to type 'number'
추가 논의
문자열 상수 방식
2번 문제의 출제자 답변에서 파생된 논제이다.
사실 책에서 내린 결론은 const enum
으로는 충분하지 않고 문자열 상수 방식을 사용하는 것이 숫자 상수 방식보다 더 안전하다 이다.
그러나 const enum으로 열거형을 선언하더라도 숫자 상수로 관리되는 열거형은 선언한 값 이외의 값을 할당하거나 접근할 때 이를 방지하지 못한다. 반면 문자열 상수 방식으로 선언한 열거형은 미리 선언하지 않은 멤버로 접근을 방지한다. 따라서 문자열 상수 방식으로 열거형을 사용하는 것이 숫자 상수 방식보다 더 안전하며 의도하지 않은 값의 할당이나 접근을 방지하는 데 도움이 된다. -p97
하지만 이 구문의 근거를 찾지 못해 해당 문제의 답변에서 제외되었다.
p97 하단 예제 3.1.6-5는 숫자 상수 방식과 문자열 상수 방식를 비교하는 예제이다.
const enum NUMBER {
ONE = 1,
TWO = 2,
}
const myNumber: NUMBER = 100; // NUMBER enum에서 100을 관리하고 있지 않지만 이는 에러를 발생시키지 않는다
const enum STRING_NUMBER {
ONE = "ONE",
TWO = "TWO",
}
const myStringNumber: STRING_NUMBER = "THREE"; // Error
숫자 상수 방식(Number
)은 에러를 반환하지 않고, 문자열 상수 방식(STRING_NUMBER
)은 에러를 반환하기 때문에 더 안전함을 근거로 들었다.
하지만 실제로는 두 방식 모두 동일한 에러를 반환했다.
const enum NUMBER {
ONE = 1,
TWO = 2,
}
const myNumber: NUMBER = 100; // 🚨 Type '100' is not assignable to type 'NUMBER'.
const enum STRING_NUMBER {
ONE = "ONE",
TWO = "TWO",
}
const myStringNumber: STRING_NUMBER = "THREE"; // 🚨 Type '"THREE"' is not assignable to type 'STRING_NUMBER'.
TS playground 실행 결과나 직접 만든 예시도 상황은 동일하다.
const enum HttpStatusType {
Forbidden = 403,
BadGateway = 502,
}
const myStatus: HttpStatusType = 430; // 🚨 Type '430' is not assignable to type 'HttpStatusType'.
그 외 chatGPT와 검색을 통해서도 명확한 정답을 찾아내기 어려움이 있었기 때문에 해당 문제의 답에 문자열 상수 방식은 포함하지 않는 것으로 정리되었다.