개념
ts에는 변수에 값을 할당하고 타입을 부여하는 방법에는 단언문과 선언문 두 가지가 있다.
선언문은 ':'을 이용해서 변수에 타입을 명시하고 단언문은 'as'를 사용해 타입을 단언한다.
interface Person { name: string };
const a: Person = {name: 'Alice'}; // 선언문
const b = {name: 'Alice'} as Person // 단언문
const c: Person = {}; // name 필드가 없으므로 에러가 난다.
// ~~~~~~~~~~~~~~~~~ Property 'name' is missing in type '{}' but required in type 'Person'.
const d = {} as Person // 에러를 던지지 않는다.
// const e = <Person>{} // 이와 같이 '단언'하는 방법도 있지만 권장되지 않는다.
결론부터 말하자면 둘 중에서 타입 선언문을 사용해야한다.
왜냐하면 선언은 할당되는 값이 타입을 만족하는 지 검사하는 반면 단언은 ts가 추론한 타입을 무시하기 때문이다.
위의 예시를 보면 변수 c는 타입을 선언했고 d는 단언했다. c 는 ts가 Person 타입과 실제 값을 검사하기 때문에 Person에 필요한 필드가 없다는 에러가 발생한다. 반면 변수 d는 타입을 강제했기 때문에 원래라면 발생했어야 할 에러가 무시된다.
구체적인 내용은 아래 예시를 통해 확인해볼 수 있다.
사용법
'x', 'y', 'z' 라는 이름을 가진 Person 배열을 만들려고 한다.
예시1.
아래와 같이 작성하면 name이라는 필드를 가진 Object Literal 배열을 만들게 된다. 아직은 Person 배열이 아니다.
const p = ['x', 'y', 'z'].map(name => ({name})) // { name: string; }[]
타입을 알려주기 위해 단언을 하면 다음과 같이 원하는 대로 Person[]을 리턴하는 것처럼 보인다.
const p2 = ['x', 'y', 'z'].map(name => ({name} as Person)) // Person[]
그러나 정말 Person 타입을 가진 배열이 된 것은 아니다.
이를 증명하기 위해 {name} 대신 {}를 넣고 정상적으로 에러를 던지는지 (위의 const c 와 같이 에러를 던져야한다) 확인해본다.
const p2check = ['x', 'y', 'z'].map(name => ({} as Person))// Person[] 에러 x
그러면 에러가 나지 않는다는 것을 확인할 수 있다.
에러를 던지지 않는다는 건 실제로 Person 타입이 아니었지만 ts의 추론을 무시하고 Person 타입을 강제한 결과이다.
그래서 Person 인터페이스에 맞지 않는데도 ts가 제대로 타입추론을 하지 못했다.
타입 선언을 이용하면 이렇게 타입 추론이 잘못된 상황에서 에러를 확인할 수 있다.
const p3 = ['x', 'y', 'z'].map(name => {
const per: Person = {name}; // <------------- 타입을 선언
return per;
}) // Person[]
const p3check = ['x', 'y', 'z'].map(name => {
const per: Person = {};
return per;
}) // Person[] 에러
위와 같이 ': Person' 으로 타입을 선언하면 {name}이 Person의 모양과 같으므로(duck-typing) 원하는대로 타입 명세를 할 수 있다.
이 때 name 필드를 제외하고 {}라고 하면, ts에서 추론된 타입과 선언된 Person 이 서로 다르므로 다음과 같은 ts 에러가 발생한다.
추가적으로 불필요한 변수를 제거하고 다음과 같이 작성할 수 있다.
const p31 = ['x', 'y', 'z'].map((name): Person => ({name})) // Person[] , name은 타입이 없고 반환이 Person이다
이렇게 타입을 선언하면 다음 두 가지 장점을 얻을 수 있다.
(1) 함수의 선언부만 읽어도 함수의 input과 output을 한 번에 확인할 수 있다.
: 함수의 input 파라미터와 output에 타입을 선언하면 다음과 같은 모양이 된다.
async findClassByStudentId(studentId: number): Promise<Class> { ...함수내용 }
함수 내용을 읽지 안더라도 학생 ID(number)를 입력 받아 교실을 return 해준다는 것을 확인할 수 있다.
(2) 정확한 곳에서 오류가 표시된다.
위의 findClassByStudentId 예시에서 리턴되는 타입을 as를 사용해 Promise<Class[]> 라고 단언할 수 있다.
async findClassByStudentId(studentId: number): Promise<Class[]> {
const result = ... (불러오는 코드) as any as Promise<Class[]>
}
이 경우 이 함수를 가져다 사용하는 곳에서는 결과가 array로 나온다고 생각하고 호출하게 될 것 이다. 실행시 ts 타입 추론은 사라지기 때문에 Runtime 환경에서는 위 함수를 호출해서 사용하는 곳 모두에서 장애가 터지게 된다. 반면 ts 추론을 할 경우 문제가 시작되는 함수 위치에서 에러를 확인할 수 있다.
Reference
Effective TypeScript 9장
'백엔드 개발 > 백엔드 일기' 카테고리의 다른 글
#063. 단건 쿼리를 복수형 쿼리로 바꾸기 + map 생성방법 (0) | 2023.09.12 |
---|---|
#025. 리팩터링: 트랜젝션 범위를 최소해 안전한 쿼리 만들기 ⛑ (2) | 2023.01.27 |
#023. 무중단 배포: Readiness 와 Liveness 설정하기 (1) | 2022.12.30 |
#022. github의 graphql explorer로 private repository 접근하기 (0) | 2022.11.09 |
#021. nginx의 reverse proxy로 cors 에러 해결하기 (0) | 2022.09.18 |