이런 경우, 구별 속성(여기선 kind)에 대해 타입 가드 스타일의 검사 (==, ===, !=, !==) 또는 switch를 사용하면 TypeScript가 특정한 리터럴을 가진 객체를 대상으로 한다는 것을 알아채고 타입 좁히기를 실행해줍니다 :)
functionarea(s:Shape) {if (s.kind ==="square") {// 이것으로 TypeScript가 `s`가 `Square`임을 알게 됨 ;)// 그러므로 `Square`의 멤버를 안전하게 사용할 수 있음 :)returns.size *s.size; }else {// `Square`가 아님? 그러면 TypeScript는// 이것이 `Rectangle`일 수 밖에 없음을 알게 됨 ;)// 그러므로 `Rectangle`의 멤버를 안전하게 사용할 수 있음 :)returns.width *s.height; }}
빠짐없는 검사(Exhaustive Check)
매우 자주 유니온의 모든 구성원들이 어떤 코드(동작)를 가지고 있음을 보장하고 싶어집니다.
interfaceSquare { kind:"square"; size:number;}interfaceRectangle { kind:"rectangle"; width:number; height:number;}// 누가 새로운 타입 `Circle`을 추가함// 이 새로운 타입에 대한 *처리가 필요한* 곳에서 TypeScript가 오류를 발생시켜주길 바람interfaceCircle { kind:"circle"; radius:number;}typeShape=Square|Rectangle|Circle;
예를 들어 조치가 필요한 부분:
functionarea(s:Shape) {if (s.kind ==="square") {returns.size *s.size; }elseif (s.kind ==="rectangle") {returns.width *s.height; }// 여기서 TypeScript가 오류를 발생시켜주면 얼마나 좋을까?}
간단하게, 조건에 걸리지 않는 블럭을 하나 추가하고 그 블럭에서 추론된 타입이 never 타입과 호환되는 것으로 정의하면 됩니다. 예제처럼 완전(exhaustive) 검사를 추가하면 보기좋게 오류가 발생합니다:
functionarea(s:Shape) {if (s.kind ==="square") {returns.size *s.size; }elseif (s.kind ==="rectangle") {returns.width *s.height; }else {// ERROR : `Circle` is not assignable to `never`const_exhaustiveCheck:never= s; }}
만약 strictNullChecks를 사용하고 완전 검사를 사용한다면, TypeScript가 "not all code paths return a value"라는 오류를 발생시킬 수 있습니다. 이건 간단히 _exhaustiveCheck (never 타입) 변수를 반환하면 해결됩니다, 이렇게:
never 타입을 받는 함수 (따라서 변수의 타입이 never로 추론될 때만 호출되는 함수)를 만들고 함수 본문이 실행될 때 오류를 던질(throw) 수 있습니다:
functionassertNever(x:never):never {thrownewError('Unexpected value. Should have been never.');}
면적 계산 함수에서 사용하는 예제:
interfaceSquare { kind:"square"; size:number;}interfaceRectangle { kind:"rectangle"; width:number; height:number;}typeShape=Square|Rectangle;functionarea(s:Shape) {switch (s.kind) {case"square": returns.size *s.size;case"rectangle": returns.width *s.height;// 컴파일 시간이 새로운 케이스가 추가되면 컴파일 오류가 발생함// 실행 시간에 새로운 값이 발생하면 런타임 오류가 발생함default: returnassertNever(s); }}
회고적(Retrospective) 버전 관리
이런 형태의 자료 구조가 있다고 하고:
typeDTO= { name:string}
이 DTO를 잔뜩 만든 다음에 name이란 선택이 마음에 들지 않게 될 수 있습니다. 이런 경우 리터럴 숫자(원한다면 문자열도 가능)로 된 새로운 유니온을 만들어서 DTO의 버전을 식별할 수 있습니다. 버전 0을 undefined로 표기하고 strictNullChecks를 활성화시키면 그냥 됩니다:
typeDTO=| { version:undefined,// 버전 0 name:string, }| { version:1, firstName:string, lastName:string,}// 다음에 또 추가| { version:2, firstName:string, middleName:string, lastName:string,} // 계속 추가
import { createStore } from'redux'typeAction= { type:'INCREMENT' }| { type:'DECREMENT' }/** * 이것은 리듀서(reducer)로, (state, action) => state 서명을 갖는 순수 함수임. * 이것은 액션이 현재 상태(state)를 다음 상태로 어떻게 전환시키는지 나타냄. * * 상태(state)의 형태는 마음대로: 기본 타입, 배열, 객체 사용 가능, * 아니면 Immutable.js 자료 구조도 사용할 수 있음. * 한가지 중요한 점은 상태 객체를 직접 변경하면 안되고, * 상태가 변경된 새로운 객체를 반환해야 한다는 것. * * 이 예제에서 우리는 `switch` 문과 문자열을 사용했지만 프로젝트 성격에 맞게 * 다른 방식을 따르는 도우미 함수(함수 맵 같은 것)도 사용할 수 있음. */functioncounter(state =0, action:Action) {switch (action.type) {case'INCREMENT':return state +1case'DECREMENT':return state -1default:return state }}// 앱의 상태를 보관하는 Redux 스토어 생성.// API는 { subscribe, dispatch, getState }.let store =createStore(counter)// 변경에 대해 subscribe() 하여 상태 변경에 따라 UI를 바꿔줄 수 있음.// 보통은 subscribe()를 직접 사용하지 않고 뷰 바인딩 라이브러리// (예를 들면, React Redux)를 사용함.// 하지만, 현재의 상태를 localStorage에 저장하는 것도 한 방법.store.subscribe(() =>console.log(store.getState()))// 내부 상태를 바꾸는 유일한 방법은 액션을 디스패치하는 것임.// 액션을 시리얼라이즈 할 수 있고 로그로 출력하거나 저장했다가 나중에// 재실행할 수 있음.store.dispatch({ type:'INCREMENT' })// 1store.dispatch({ type:'INCREMENT' })// 2store.dispatch({ type:'DECREMENT' })// 1
TypeScript를 사용하면 오타로 인한 오류를 방지할 수 있고 리팩터링 편의성이 향상되며 코드가 좀더 자체 문서화되게 할 수 있습니다.