2023-04-27 · 17 min read · 학습
러닝 타입스크립트 CHAPTER 9 - 타입 제한자
top 타입
접근 불가능한 타입을 설명하기 위해 bottom 타입을 사용하듯이 그 반대 개념도 타입 이론에 존재합니다. top 타입은 시스템에서 가능한 모든 값을 나타내는 타입입니다. 모든 다른 타입의 값은 top 타입에 할당, 제공될 수 있습니다.
any 다시 보기
any 타입은 모든 타입의 위치에 제공될 수 있다는 점에서 top 타입처럼 작동할 수 있습니다.
다만, any는 타입스크립트가 해당 값에 대한 할당 가능성 또는 멤버에 대해 타입 검사를 수행하지 않도록 명시적으로 지시한다는 문제점을 갖습니다. 이러한 안정성 부족은 타입스크립트의 유용성을 줄입니다.
function greetComedian(name: any) { // 타입 오류 없음 console.log(`Announcing ${name.toUpperCase()}!`)}
greetComedian({ name: 'Bea Arthur' })// Runtime error: name.toUpperCase is not a function어떤 값이든 될 수 있음을 나타내려면 unknown 타입이 훨씬 안전합니다.
unknown
타입스크립트에서 unknown 타입은 진정한 top 타입입니다. 모든 객체를 unknown 타입의 위치로 전달할 수 있다는 점에서 any 타입과 유사합니다. unknown 타입과 any 타입의 주요 차이점은 unknown 타입의 값을 훨씬 제한적으로 취급한다는 점입니다.
- 타입스크립트는 unknown 타입 값의 속성에 직접 접근할 수 없습니다.
- unknown 타입은 top 타입이 아닌 타입에는 할당할 수 없습니다.
다음 코드는 unknown 타입 값의 속성에 접근하려고 시도하는 경우입니다.
function greetComedian(name: unknown) { console.log(`Announcing ${name.toUpperCase()}!`) // Error: Object is of type 'unknown'.}타입스크립트가 unknown 타입인 name에 접근할 수 있는 유일한 방법은 instanceof 나 typeof 또는 타입 어서션을 사용하는 것처럼 값의 타입이 제한된 경우입니다.
function greetComedianSafety(name: unknown) { if (typeof name === 'string') { console.log(`Announcing ${name.toUpperCase()}!`) // OK } else { console.log("Well, I'm off.") }}타입 서술어
앞서 살펴본 방식으로 타입을 좁힐 수 있지만 로직을 함수로 감싸면 타입을 좁힐 수 없게 됩니다.
function isNumberOrString(value: unknown) { return ['number', 'string'].includes(typeof value)}
function logValueIfExists(value: number | string | null | undefined) { if (isNumberOrString(value)) { value.toString() } else { console.log('Value does not exist:', value) }}코드상으로는 isNumberOrString(value)가 true를 반환하여 if문 내부의 값이 두 가지 타입 중 하나로 유추할 수 있지만,
타입스크립트는 그렇지 않습니다.
타입스크립트는 isNumberOrString가 boolean 값을 반환한다는 사실만 알 수 있고,
인수의 타입을 좁히기 위함이라는 건 알 수 없습니다.
인수가 특정 타입인지 여부를 나타내기 위해 boolean 값을 반환하는 함수를 위한 특별한 구문을 사용할 수 있습니다. 이를 타입 서술어(type predicate)라고 부르며 사용자 정의 타입 가드라고도 부릅니다. 타입 서술어는 일반적으로 매개변수로 전달된 인수가 매개변수의 타입보다 더 구체적인 타입인지 여부를 나타내는 데 사용됩니다.
타입 서술어의 반환 타입은 매개변수의 이름, is 키워드, 특정 타입으로 선언할 수 있습니다.
function typePredicate(input: WideType): input is NarrowTypeisNumberOrString에 활용하면 다음과 같습니다.
function isNumberOrString(value: unknown): value is number | string { return ['number', 'string'].includes(typeof value)}
function logValueIfExists(value: number | string | null | undefined) { if (isNumberOrString(value)) { // value: number | string의 타입 value.toString() } else { // value: null | undefined의 타입 console.log('Value does not exist:', value) }}타입 서술어는 단순히 boolean 값을 반환하는 것이 아니라 인수가 더 구체적인 타입임을 나타내는 것이라고 생각할 수 있습니다.
타입 서술어는 이미 한 인터페이스의 인스턴스로 알려진 객체가 더 구체적인 인터페이스의 인스턴스인지 여부를 검사하는데 자주 사용됩니다.
interface Comedian { funny: boolean}
interface StandupComedian extends Comedian { routine: string}
function isStandupComedian(value: Comedian): value is StandupComedian { return 'routine' in value}
function workWithComedian(value: Comedian) { if (isStandupComedian(value)) { // value: StandupComedian console.log(value.routine) // OK }
console.log(value.routine) // Error: Property 'routine' does not exist on type 'Comedian'.}타입 서술어는 속성이나 값의 타입을 확인하는 것 이상을 수행해 잘못 사용하기 쉬우므로 간단한 타입 서술어만을 사용하는 것이 좋습니다.
타입 연산자
키워드나 기존 타입의 이름만 사용해 모든 타입을 나타낼 수 없습니다. 때로는 기존 타입의 속성 일부를 변환해서 두 타입을 결합하는 새로운 타입을 생성해야 할 때도 있습니다.
keyof
자바스크립트 객체는 일반적으로 string 타입인 동적값을 사용하여 검색된 멤버를 갖습니다. 이러한 키를 타입 시스템에서 표현하려면 유효하지 않은 키도 허용할 수 있어 상당히 까다로울 수 있습니다.
interface Ratings { audience: number critics: number}
function getRating(ratings: Ratings, key: string): number { return ratings[key] // Error: Element implicitly has an 'any' type because expression // of type 'string' can't be used to index type 'Ratings'. // No index signature with a parameter of type 'string' was found on type 'Ratings'.}
const ratings: Ratings = { audience: 66, critics: 84 }
getRating(ratings, 'audience') // OKgetRating(ratings, 'not valid') // 허용되지만 사용하면 안 됨이에 리터럴 유니언 타입을 사용하여 컨테이너 값에 존재하는 키를 적절하게 제한할 수 있습니다.
interface Ratings { audience: number critics: number}
function getRating(ratings: Ratings, key: 'audience' | 'critics'): number { return ratings[key]}
const ratings: Ratings = { audience: 66, critics: 84 }
getRating(ratings, 'audience') // OKgetRating(ratings, 'not valid')// Error: Argument of type '"not valid"' is not// assignable to parameter of type '"audience" | "critics"'.하지만 인터페이스에 수십 개 이상의 멤버가 있다면 이 작업은 상당히 번거로울 것입니다.
대신 타입스크립트에서는 기존에 존재하는 타입을 사용하고,
해당 타입에 허용되는 모든 키의 조합을 반환하는 keyof 연산자를 제공합니다.
interface Ratings { audience: number critics: number}
function getRating(ratings: Ratings, key: keyof Ratings): number { return ratings[key]}
const ratings: Ratings = { audience: 66, critics: 84 }
getRating(ratings, 'audience') // OKgetRating(ratings, 'not valid')// Error: Argument of type '"not valid"' is not// assignable to parameter of type '"audience" | "critics"'.keyof는 존재하는 타입의 키를 바탕으로 유니언 타입을 생성하는 훌륭한 기능입니다.
typeof
typeof는 제공되는 값의 타입을 반환합니다.
typeof 타입 연산자는 시각적으로 주어진 값이 어떤 타입인지를 반환할 때 사용하는 런타임 typeof 연산자처럼 보이지만 이 둘은 차이가 있습니다.
자바스크립트의 typeof 연산자는 타입에 대한 문자열 이름을 반환하는 런타임 연산입니다. 타입스크립트의 typeof 연산자는 타입스크립트에서만 사용할 수 있으며 컴파일된 자바스크립트 코드에는 나타나지 않습니다.
keyof typeof
typeof는 값의 타입을 검색하고,
keyof는 타입에 허용된 키를 검색합니다.
두 키워드를 연결해 값의 타입에 허용된 키를 간결하게 검색할 수 있습니다.
const ratings = { imdb: 8.4, metacritic: 82,}
function logRating(key: keyof typeof ratings) { console.log(ratings[key])}
logRating('imdb') // OKlogRating('invalid')// Error: Argument of type '"invalid"' is not assignable to parameter of type '"imdb" | "metacritic"'.타입 어서션
타입스크립트는 코드가 강력하게 타입화될 때 가장 잘 작동합니다.
그러나 경우에 따라 타입 시스템에 100% 정확하게 알리는 것이 불가능할 때도 있습니다.
예를 들어 JSON.parse는 의도적으로 top 타입인 any를 반환합니다.
타입스크립트는 값의 타입에 대한 타입 시스템의 이해를 재정의하기 위한 구문으로 타입 어서션(type assertion)을 제공합니다.
as 키워드를 통해 값을 해당 타입으로 처리합니다.
const rawData = '["grace", "frankie"]'
// 타입: anyJSON.parse(rawData)
// 타입: string[]JSON.parse(rawData) as string[]
// 타입: [string, string]JSON.parse(rawData) as [string, string]
// 타입: ["grace", "frankie"]JSON.parse(rawData) as ['grace', 'frankie']타입 어서션은 타입 시스템에만 존재하며 자바스크립트로 컴파일될 때 다른 타입 시스템 구문과 함께 제거됩니다. 타입스크립트 모범 사례는 가능한 한 타입 어서션을 사용하지 않는 것입니다. 코드가 완전히 타입화되고 어서션을 통해 타입 이해를 방해할 필요가 없는 것이 가장 좋습니다. 하지만, 타입 어서션은 유용하고 때때로 필요한 경우가 있습니다.
포착된 오류 타입 어서션
오류를 처리할 때 타입 어서션이 매우 유용할 수 있습니다.
try { // 오류 발생 코드} catch (error) { console.warn('Oh no!', (error as Error).message)}발생된 오류가 예상된 오류 타입인지 확인하기 위해 instanceof와 같은 타입 내로잉을 사용할 수 있습니다.
try { // 오류 발생 코드} catch (error) { console.warn('Oh no!', error instanceof Error ? erorr.message : error)}non-null 어서션
실제로는 아니고 이론적으로만 null 또는 undefined를 포함할 수 있는 변수에서 null과 undefined를 제거할 때 타입 어서션을 주로 사용합니다.
// 타입 유추: Date | undefinedlet maybeDate = Math.random() > 0.5 ? undefined : new Date()
// 타입이 Date라고 간주됨maybeDate as DatemaybeDate!타입 어서션 주의 사항
any 타입과 마찬가지로 타입 어서션은 타입 시스템에 필요한 하나의 도피 수단입니다. 타입 어서션은 사용하는 것이 안전하다고 확실히 확신할 때만 사용합니다.
어서션 vs 선언
변수 타입을 선언하기 위해 타입 애너테이션을 사용하는 것과 초깃값으로 변수 타입을 변경하기 위해 타입 어서션을 사용하는 것 사이에는 차이가 있습니다.
interface Entertainer { acts: string[] name: string}
const declared: Entertainer = { // Error: Property 'acts' is missing in type '{ name: string; }' but required in type 'Entertainer'. name: 'Moms Mabley',}
const asserted = { name: 'Moms Mabley',} as Entertainer // 허용되지만 런타임 시 오류 발생어서션 할당 가능성
타입 어서션은 일부 값의 타입이 약간 잘못된 상황에서 필요한 작은 도피 수단입니다. 완전히 서로 관련없는 타입에서는 오류가 감지됩니다.
let myValue = 'Stella!' as number이중 타입 어서션을 사용할 수도 있지만, 이는 위험한 방법입니다.
let myValueDouble = '1337' as unknown as number // 허용되지만 이렇게 사용하면 안 됨const 어서션
const 어서션은 배열, 원시 타입, 값, 별칭 등 모든 값을 상수로 취급해야 함을 나타내는데 사용합니다.
as const를 수신하는 모든 타입에는 다음 규칙이 적용됩니다.
- 배열은 가변 배열이 아니라 읽기 전용 튜플로 취급
- 리터럴은 일반적인 원시 타입과 동등하지 않고 리터럴로 취급
- 객체의 속성은 읽기 전용으로 간주
// 타입: (number | string)[];[0, ''][ // 타입: readonly [0, ''] (0, '')] as const리터럴에서 원시 타입으로
타입 시스템이 리터럴 값을 일반적인 원시 타입으로 확장하기보다 특정 리터럴로 이해하는 것이 유용할 수 있습니다.
// 타입: () => stringconst getName = () => 'Maria Bamford'
// 타입: () => 'Maria Bamford'const getNameConst = () => 'Maria Bamford' as const읽기 전용 객체
객체에서 as const를 사용해 값 리터럴을 어서션하면 유추된 타입이 가능한 한 구체적으로 전환됩니다.
모든 멤버 속성은 readonly가 되고,
리터럴은 고유한 타입으로 간주되며,
배열은 읽기 전용 튜플이 됩니다.
function describePreference(preference: 'maybe' | 'no' | 'yes') { switch (preference) { case 'maybe': return 'I suppose...' case 'no': return 'No thanks.' case 'yes': return 'Yes please!' }}
// 타입 { movie: string, standup: string }const preferencesMutable = { movie: 'maybe', standup: 'yes',}
describePreference(preferencesMutable.movie)// Error: Argument of type 'string' is not assignable to parameter of type '"maybe" | "no" | "yes"'.
preferencesMutable.movie = 'no' // OK
// 타입 { readonly movie: 'maybe', readonly standup: 'yes' }const preferencesReadonly = { movie: 'maybe', standup: 'yes',} as const
describePreference(preferencesReadonly.movie) // OK
preferencesReadonly.movie = 'no'// Error: Cannot assign to 'movie' because it is a read-only property.마치며
- top 타입: 매우 허용도가 높은 any와 제한적인 unknown
- 타입 연산자: keyof로 타입의 키를 알아내고, typeof로 값의 타입 알아내기
- 값의 타입을 변경하기 위해 타입 어서션을 사용하는 경우와 사용하지 않는 경우
- as const 어서션을 사용한 타입 내로잉