초과 프로퍼티
TypeScript가 한 객체 타입을 다른 객체 타입에 할당할 수 있는지 확인할 때도 타입 넓히기를 이용합니다.
객체 타입과 그 멤버들은 공변 관계라는 규칙만을 적용하면 문제가 발생할 수 있습니다.
예를 들어 다음처럼 클래스에 전달해 그 내부 상태를 설정하는 용도의 Options라는 객체가 있다고 해보겠습니다.
type Options = {
baseURL: string
cacheSize?: number
tier?: 'prod' | 'dev'
}
class API {
constructor(private options: Options){}
}
new API({
baseURL: 'https://cozy-linda.tistory.com',
tier: 'prod'
})
옵션의 철자가 틀리면 어떤 일이 발생할까요?
type Options = {
baseURL: string
cacheSize?: number
tier?: 'prod' | 'dev'
}
class API {
constructor(private options: Options){}
}
new API({
baseURL: 'https://cozy-linda.tistory.com',
// '{ baseURL: string; teir: string; }' 형식의 인수는 'Options' 형식의 매개 변수에 할당될 수 없습니다.
// 개체 리터럴은 알려진 속성만 지정할 수 있으며 'Options' 형식에 'teir'이(가) 없습니다.ts(2345)
teir: 'prod'
})
JavaScript로 작업할 때 흔히 볼 수 있는 버그라서, 개발에 큰 도움을 주는 TypeScript의 특성입니다.
하지만 객체 타입은 그 멤버와 공변이라 했는데 TypeScript는 이를 어떻게 검출할 수 있을까요?
구체적으로 다음과 같은 일이 일어났습니다.
- {baseURL: string, cacheSize?: number, tier?: 'prod' | 'dev'} 타입이 필요합니다.
- {baseURL: string, teir: string)을 전달했습니다.
- 필요한 타입의 서브 타입을 전달했고 TypeScript는 이를 에러로 판단했습니다.
TypeScript가 이를 검출할 수 있었던 건 초과 프로퍼티 확인(excess property checking) 기능 덕분입니다.
신선한(fresh) 객체 리터럴 타입 T를 다른 타입 U에 할당하려는 상황에서 T가 U엔 존재하지 않는 프로퍼티를 가지고 있다면 TypeScript는 이를 에러로 처리합니다.
여기서 '신선한 객체 리터럴 타입'이란 TypeScript가 객체 리터럴로부터 추론한 타입을 가리킵니다.
객체 리터럴이 타입 어서션을 사용하거나 변수로 할당되면 신선한 객체 리터럴 타입은 일반 객체 타입으로 넓혀지면서 신선함은 사라집니다.
여러 내용을 축약하고 있는 설명이므로 다시 예제를 이용해 더 다양한 변형을 시도해 보겠습니다.
type Options = {
baseURL: string
cacheSize?: number
tier?: 'prod' | 'dev'
}
class API {
constructor(private options: Options){}
}
new API ({ // ①
baseURL: 'https://cozy-linda.tistory.com',
tier: 'prod'
})
new API({ // ②
baseURL: 'https://cozy-linda.tistory.com',
// '{ baseURL: string; badTeir: string; }' 형식의 인수는 'Options' 형식의 매개 변수에 할당될 수 없습니다.
// 개체 리터럴은 알려진 속성만 지정할 수 있으며 'Options' 형식에 'badTeir'이(가) 없습니다.ts(2345)
badTeir: 'prod'
})
new API({ // ③
baseURL: 'https://cozy-linda.tistory.com',
badTier: 'prod'
} as Options)
let badOptions = { // ④
baseURL: 'https://cozy-linda.tistory.com',
badTier: 'prod'
}
new API(badOptions)
let options: Options = { // ⑤
baseURL: 'https://cozy-linda.tistory.com',
// '{ baseURL: string; badTier: string; }' 형식은 'Options' 형식에 할당할 수 없습니다.
// 개체 리터럴은 알려진 속성만 지정할 수 있으며 'Options' 형식에 'badTier'이(가) 없습니다.ts(2322)
badTier: 'prod'
}
new API(options)
- baseURL 그리고 두 개의 선택형 프로퍼티 중 하나인 tier로 API를 인스턴스화합니다. (잘 동작함.)
- tier를 badTier로 잘못 입력했습니다. new API에 전달한 옵션 객체는 신선한 객체이므로 TypeScript는 초과 프로퍼티 확인을 시작하며 badTier가 초과된 프로퍼티임을 알아냅니다.
- 유효하지 않은 옵션 객체를 Options 타입이라고 어서션했습니다. TypeScript는 더 이상 옵션 객체를 신선한 것으로 취급하지 않으므로 초과 프로퍼티 확인을 수행하지 않으며, 따라서 아무런 에러도 발생하지 않습니다.
- 옵션 객체를 변수 badOptions에 할당했습니다. TypeScript는 이 객체를 더 이상 신선한 객체로 보지 않으므로 초과 프로퍼티 확인을 수행하지 않으며, 따라서 아무 에러도 발생하지 않습니다.
- options의 타입을 Options라고 명시하면 options에 할당된 객체는 신선한 객체로 취급합니다. 따라서 TypeScript는 초과 프로퍼티 확인을 수행하고 버그를 찾아냅니다. 이 예제에서 초과 프로퍼티 확인은 options를 new API로 전달할 때가 아니라 옵션 객체를 options 변수로 할당할 때 수행됩니다.
이런 규칙을 모두 외울 필요 없습니다:)
이들은 실용적으로 많은 버그를 잡을 수 있도록 동작하는 TypeScript의 내부 규칙으로, 프로그래머에겐 아무런 부담을 추가하지 않습니다.
'👶 TypeScript' 카테고리의 다른 글
정제 - 차별된 유니온 타입 (0) | 2023.01.19 |
---|---|
정제 (0) | 2023.01.19 |
타입 넓히기 - const 타입 (0) | 2023.01.18 |
할당성(assignability) (0) | 2023.01.18 |
함수 가변성 (0) | 2023.01.18 |