TypeScript Deep Dive
  • README
  • 시작하기
    • 왜 타입스크립트인가
  • 자바스크립트
    • 비교 연산자
    • 참조 연산자
    • Null vs. Undefined
    • this
    • 클로저
    • Number
    • Truthy
  • 미래의 자바스크립트
    • 클래스
      • 즉시실행함수
    • 화살표 함수
    • 나머지 연산자
    • let
    • const
    • 비구조화 할당
    • 전개 연산자
    • for...of
    • 이터레이터
    • 템플릿 리터럴
    • 프로미스
    • 제네레이터
    • Async Await
  • 프로젝트
    • 컴파일러 제어
      • tsconfig.json
      • 파일 경로 지정
    • 선언
    • 모듈화
      • 파일을 이용한 모듈화
      • globals.d.ts
    • 네임스페이스
    • 동적 표현식 가져오기
  • Node.js 시작하기
  • Browser 시작하기
  • 타입스크립트 타입 시스템
    • 자바스크립트 마이그레이션 가이드
    • @types
    • 주변 선언
      • 파일 선언
      • 변수
    • 인터페이스
    • 열거형(Enums)
    • lib.d.ts
    • 함수
    • 콜러블(Callable)
    • 타입 표명(Type Assertion)
    • 신선도(Freshness)
    • 타입 가드
    • 리터럴(Literal)
    • 읽기 전용(readonly)
    • 제네릭
    • 타입 인터페이스
    • 타입 호환성
    • Never 타입
    • 구별된 유니온
    • 인덱스 서명(Index Signature)
    • 타입 이동하기
    • 예외 처리
    • 믹스인(Mixin)
  • JSX
    • React
    • Non React JSX
  • Options
    • noImplicitAny
    • strictNullChecks
  • 타입스크립트 에러
    • 에러 메세지
    • 공통 에러
  • NPM
  • 테스트
    • Jest
    • Cypress
  • Tools
    • Prettier
    • Husky
    • ESLint
    • Changelog
  • 팁
    • 문자열 Enums
    • 타입 단언
    • 상태 저장 함수
    • 커링
    • 제네릭 타입 예시
    • 객체 타입 설정
    • 유용한 클래스
    • Import / Export
    • 속성 Setters
    • outFile 주의사항
    • 제이쿼리 팁
    • 정적 생성자
    • 싱글톤 패턴
    • 함수 파라미터
    • 토글 생성
    • Import 여러개 하기
    • 배열 생성
    • 생성자에서 타입정의
  • 스타일 가이드
  • 타입스크립트 컴파일러 구조
    • Program
    • AST
      • TIP: Visit Children
      • TIP: SyntaxKind enum
      • Trivia
    • Scanner
    • Parser
      • Parser Functions
    • Binder
      • Binder Functions
      • Binder Declarations
      • Binder Container
      • Binder SymbolTable
      • Binder Error Reporting
    • Checker
      • Checker Diagnostics
      • Checker Error Reporting
    • Emitter
      • Emitter Functions
      • Emitter SourceMaps
    • Contributing
Powered by GitBook
On this page
  • 구별된 유니온(Discriminated Union)
  • 빠짐없는 검사(Exhaustive Check)
  • Switch
  • strictNullChecks
  • 완전 검사에서 Throw
  • 회고적(Retrospective) 버전 관리
  • Redux

Was this helpful?

  1. 타입스크립트 타입 시스템

구별된 유니온

PreviousNever 타입Next인덱스 서명(Index Signature)

Last updated 3 years ago

Was this helpful?

구별된 유니온(Discriminated Union)

이 있는 클래스가 있다면 그 속성으로 유니온 구성원을 구별할 수 있습니다.

예제로 Square 타입과 Rectangle 타입의 유니온을 생각해봅시다. 여기에 kind라는 멤버 속성이 있는데, 이 속성은 모든 유니온 구성원에 존재하는 리터럴 타입입니다:

interface Square {
    kind: "square";
    size: number;
}

interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}
type Shape = Square | Rectangle;

이런 경우, 구별 속성(여기선 kind)에 대해 타입 가드 스타일의 검사 (==, ===, !=, !==) 또는 switch를 사용하면 TypeScript가 특정한 리터럴을 가진 객체를 대상으로 한다는 것을 알아채고 타입 좁히기를 실행해줍니다 :)

function area(s: Shape) {
    if (s.kind === "square") {
        // 이것으로 TypeScript가 `s`가 `Square`임을 알게 됨 ;)
        // 그러므로 `Square`의 멤버를 안전하게 사용할 수 있음 :)
        return s.size * s.size;
    }
    else {
        // `Square`가 아님? 그러면 TypeScript는
        // 이것이 `Rectangle`일 수 밖에 없음을 알게 됨 ;)
        // 그러므로 `Rectangle`의 멤버를 안전하게 사용할 수 있음 :)
        return s.width * s.height;
    }
}

빠짐없는 검사(Exhaustive Check)

매우 자주 유니온의 모든 구성원들이 어떤 코드(동작)를 가지고 있음을 보장하고 싶어집니다.

interface Square {
    kind: "square";
    size: number;
}

interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}

// 누가 새로운 타입 `Circle`을 추가함
// 이 새로운 타입에 대한 *처리가 필요한* 곳에서 TypeScript가 오류를 발생시켜주길 바람
interface Circle {
    kind: "circle";
    radius: number;
}

type Shape = Square | Rectangle | Circle;

예를 들어 조치가 필요한 부분:

function area(s: Shape) {
    if (s.kind === "square") {
        return s.size * s.size;
    }
    else if (s.kind === "rectangle") {
        return s.width * s.height;
    }
    // 여기서 TypeScript가 오류를 발생시켜주면 얼마나 좋을까?
}

간단하게, 조건에 걸리지 않는 블럭을 하나 추가하고 그 블럭에서 추론된 타입이 never 타입과 호환되는 것으로 정의하면 됩니다. 예제처럼 완전(exhaustive) 검사를 추가하면 보기좋게 오류가 발생합니다:

function area(s: Shape) {
    if (s.kind === "square") {
        return s.size * s.size;
    }
    else if (s.kind === "rectangle") {
        return s.width * s.height;
    }
    else {
        // ERROR : `Circle` is not assignable to `never`
        const _exhaustiveCheck: never = s;
    }
}

그러면 새로 추가된 경우를 처리하며 됩니다:

function area(s: Shape) {
    if (s.kind === "square") {
        return s.size * s.size;
    }
    else if (s.kind === "rectangle") {
        return s.width * s.height;
    }
    else if (s.kind === "circle") {
        return Math.PI * (s.radius **2);
    }
    else {
        // 오케이, 한번 더
        const _exhaustiveCheck: never = s;
    }
}

Switch

팁: 당연히 switch 문에서 동일하게 할 수 있습니다:

function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.width * s.height;
        case "circle": return Math.PI * s.radius * s.radius;
        default: const _exhaustiveCheck: never = s;
    }
}

strictNullChecks

만약 strictNullChecks를 사용하고 완전 검사를 사용한다면, TypeScript가 "not all code paths return a value"라는 오류를 발생시킬 수 있습니다. 이건 간단히 _exhaustiveCheck (never 타입) 변수를 반환하면 해결됩니다, 이렇게:

function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.width * s.height;
        case "circle": return Math.PI * s.radius * s.radius;
        default:
          const _exhaustiveCheck: never = s;
          return _exhaustiveCheck;
    }
}

완전 검사에서 Throw

never 타입을 받는 함수 (따라서 변수의 타입이 never로 추론될 때만 호출되는 함수)를 만들고 함수 본문이 실행될 때 오류를 던질(throw) 수 있습니다:

function assertNever(x:never): never {
    throw new Error('Unexpected value. Should have been never.');
}

면적 계산 함수에서 사용하는 예제:

interface Square {
    kind: "square";
    size: number;
}
interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}
type Shape = Square | Rectangle;

function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.width * s.height;
        // 컴파일 시간이 새로운 케이스가 추가되면 컴파일 오류가 발생함
        // 실행 시간에 새로운 값이 발생하면 런타임 오류가 발생함
        default: return assertNever(s);
    }
}

회고적(Retrospective) 버전 관리

이런 형태의 자료 구조가 있다고 하고:

type DTO = {
  name: string
}

이 DTO를 잔뜩 만든 다음에 name이란 선택이 마음에 들지 않게 될 수 있습니다. 이런 경우 리터럴 숫자(원한다면 문자열도 가능)로 된 새로운 유니온을 만들어서 DTO의 버전을 식별할 수 있습니다. 버전 0을 undefined로 표기하고 strictNullChecks를 활성화시키면 그냥 됩니다:

type DTO = 
| { 
   version: undefined, // 버전 0
   name: string,
 }
| {
   version: 1,
   firstName: string,
   lastName: string, 
}
// 다음에 또 추가
| {
    version: 2,
    firstName: string,
    middleName: string,
    lastName: string, 
} 
// 계속 추가

이렇게 DTO를 사용하는 예제:

function printDTO(dto:DTO) {
  if (dto.version == null) {
      console.log(dto.name);
  } else if (dto.version == 1) {
      console.log(dto.firstName,dto.lastName);
  } else if (dto.version == 2) {
      console.log(dto.firstName, dto.middleName, dto.lastName);
  } else {
      const _exhaustiveCheck: never = dto;
  }
}

Redux

이 기능을 활용하는 유명 라이브러리가 redux 입니다.

import { createStore } from 'redux'

type Action
  = {
    type: 'INCREMENT'
  }
  | {
    type: 'DECREMENT'
  }

/**
 * 이것은 리듀서(reducer)로, (state, action) => state 서명을 갖는 순수 함수임.
 * 이것은 액션이 현재 상태(state)를 다음 상태로 어떻게 전환시키는지 나타냄.
 *
 * 상태(state)의 형태는 마음대로: 기본 타입, 배열, 객체 사용 가능,
 * 아니면 Immutable.js 자료 구조도 사용할 수 있음.
 * 한가지 중요한 점은 상태 객체를 직접 변경하면 안되고,
 * 상태가 변경된 새로운 객체를 반환해야 한다는 것.
 *
 * 이 예제에서 우리는 `switch` 문과 문자열을 사용했지만 프로젝트 성격에 맞게
 * 다른 방식을 따르는 도우미 함수(함수 맵 같은 것)도 사용할 수 있음.
 */
function counter(state = 0, action: Action) {
  switch (action.type) {
  case 'INCREMENT':
    return state + 1
  case 'DECREMENT':
    return state - 1
  default:
    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' })
// 1
store.dispatch({ type: 'INCREMENT' })
// 2
store.dispatch({ type: 'DECREMENT' })
// 1

TypeScript를 사용하면 오타로 인한 오류를 방지할 수 있고 리팩터링 편의성이 향상되며 코드가 좀더 자체 문서화되게 할 수 있습니다.

이것은 에 TypeScript 타입 어노테이션이 추가된 모습입니다:

리터럴 멤버 속성
리덕스의 주요 내용