[Type Script] 체크하는 시간을 줄이기 위해 하형 정의를 조금 노력합니다.

26448 단어 TypeScripttech
스타일 썼어요?
Type Script에서는 대체로 다음과 같은 경우 형식을 써야 합니다.
  • 소스 코드 외부에서 제공된 정보(예: API 응답)에 유형을 제공할 때
  • 매개 변수나 반환값any 또는 unknwon에 정의된 의욕이 없는 라이브러리의 반환값에 유형 정보를 제공할 때
  • 함수를 정의할 때
  • 이때 번거로움 때문에 우직한 정의를 내리면if문으로 억지로 쓰기 보호에 빠지고 오히려 더 많은 번거로움이 생길 수 있다.
    기계적으로 시키고 싶지만 지금은 사람이 쓸 수밖에 없기 때문에 어쨌든 귀찮아요.
    하지만 형식 정의에 공을 들이면 한꺼번에 가벼워질 수 있을까?이에 따라 평소 사용하는 수법 3가지를 열거하기로 했다.
    검은색 마술을 쓰지 않고 직감적으로 쓴 것만 열거했기 때문에 내용을 긴장되지 않게 읽을 수 있었으면 좋겠다.

    공통 항목이 없는 객체 유형 전환


    공통 없음은 아래 설명된 A와 B의 공통 속성이 전혀 없는 경우를 의미합니다.
    type A = {
     x: number
    }
    
    type B = {
     y: number
    }
    
    이것은 API와 교환할 때 응답 내용이 상태에 따라 완전히 다른 경우를 자주 겪는다.
    이러한 API의 응답 유형 정의를 고려합니다.
    예컨대
    "로그인 전에는 session 속성이 있고, 로그인 후에는 token 속성과 user 속성이 있습니다."
    이런 패턴.
    이것은 다음과 같은 정의가 없습니까?
    type UserInfo = {
      user?: {...}
      session?: string
      token?: string
    }
    
    네, 모든 것이 완벽합니다.이러다가는 무한 필기형 방어의if문에 빠질 수도 있으니 포기하자.
    아래와 같이 각 장소의 유형을 잘 정의하는 것이 좋다.
    type NoLoginUserInfo = {
      session: string
    }
    
    type LoginedUserInfo = {
      user: {...},
      token: string
    }
    
    export type UserInfo = NoLoginUserInfo | LoginedUserInfo
    
    
    이렇게 하면 다음과 같은 in 판정을 통해 한쪽에만 존재하는 키의 존재를 정확하게 각자의 유형으로 추론할 수 있다.
    그런 판정용 함수를 쓸 필요가 없다.
    function checkLogin(userInfo: UserInfo) {
      if ("token" in userInfo) {
        // userInfo が LoginedUserInfo として推論される
      } else {
        // userInfo が NoLoginUserInfo として推論される
      }
    }
    
    이 정도의 유형 검사가 있다면 원래 자바스크립트도 해야 할 것 같다.
    Type Script의 경우에만 if문을 쓰면 아무리 생각해도 촌스럽기 때문에 이렇게 유형을 정해서 AltJS의 스타일을 보여드리는 게 좋을 것 같아요.

    특정 유형의 단서가 있는 대상에 대해


    앞의 예와 달리 지금은 대상 내에 특정 종류의 단서 속성이 있는 상황이다.
    예를 들어 직원 정보를 표현하는 유형을 고려한다.
    type ContractType = "fulltime" | "parttime"
    
    type Employee = {
      contractType: ContractType
      department: string
      salary: number
      bonus?: number
      period?: number
    }
    
    이것은contractTypeisLoginedUser(user: UserInfo): user is LoginedUserInfo이다. 즉, 정규직은 무기고용이고period,bonus가 없다.
    fulltime, 즉 아르바이트 직원의 경우 유기 고용이기 때문에 페리코드가 있고 보너스가 없다는 것이다.
    그러나 이 유형의 정의라면 다음과 같은if문구로 검사할 때 두 번의 번거로움이 발생합니다.
    function calcSalary(employee: Employee) {
      if (employee.contractType === "fulltime") {
        // fulltime なので bonus が仕様上あるはずだが、TypeScriptがそのことを知らないためエラーになる
       return employee.salary + employee.bonus
        // parttime なので period があるはずだが以下略
      } else if (employee.period < Date.now()) {
        return employee.salary
      } else {
        return 0 // 悲しい
      }
    }
    
    이것은 다음과 같은 방식으로 유형 정의를 해결할 수 있다.
    type FulltimeEmployee = {
      contractType: "fulltime"
      department: string
      salary: number
      bonus: number
    }
    
    type ParttimeEmployee = {
      contractType: "parttime"
      department: string
      salary: number
      period: number
    }
    
    type Employee = FulltimeEmployee | ParttimeEmployee
    
    이렇게 함으로써 다음과 같은 유형 추론 결과를 얻었다.
    좀 신경 쓰일 수도 있고
    function calcSalary(employee: Employee) {
      if (employee.contractType === "fulltime") {
        // ここで employee が FulltimeEmployee  に推論されるためエラーにならない
       return employee.salary + employee.bonus
        // ここで employee が ParttimeEmployee  に推論されるためエラーにならない
      } else if (employee.period < Date.now()) {
        return employee.salary
      } else {
        return 0 // 悲しさは変わらない
      }
    }
    
    parttime의 유형 정의는 contractType가 아니지만, 단원 테스트를 잘 쓰면 오류가 무너질 수 있습니다.
    형식적인 정의에 있어서 모든 노력을 기울일 필요가 없다.
    참고로 Redux에서 이 규격을 잘 사용하면 Reducter의 정의가 매우 편안할 것입니다.
    const COUNT_UP = "COUNT_UP" as const
    const CountUp = (increases: number) => ({
      type: COUNT_UP,
      payload: {
        increases
      }
    })
    
    const COUNT_DOWN = "COUNT_DOWN" as const
    const CountUp = (decreases: number) => ({
      type: COUNT_DOWN,
      payload: {
        decreases
      }
    })
    
    type Actions = ReturnType<
      | typeof CountUp
      | typeof CountDown
    >
      
    function reducer(state: State, action: Actions): State {
      switch(action.type) {
        case COUNT_UP: {
          return {
            ...state,
    	// action.type を手がかりに、 payload の型が推論されている
            count: state.count + action.payload.increases
          };
        }
        case COUNT_DOWN: {
          return {
            ...state,
            count: state.count - action.payload.decreases
          };
        }
      }
      return state
    }  
    

    nullable의 속성과 잘 어울려요.


    Optional Chaning 기능이 있습니다.
    nullable 객체ContractType에 액세스할 수 있습니다.
    이것은 사용하기에 의외의 어려움이다. 만약 그렇게 사용한다면 결과적으로undefined가 멈추게 될 것이다.
    type Foo = {
      hoge?: {
        x: number
      }
      fuga?: {
        y: number
      }
    }
    
    function doSomething(foo: Foo) {
      return foo.hoge?.x + foo.fuga?.y //  各項が number | undefined になるので足し算が出来ない
    }
    
    어떻게 사용하면 좋을까, 이런 말과 주제가 좀 다르기 때문에 여기서 이 Optional Chaning의 존재를 전제로 유형 정의를 검토해 보려고 합니다.
    예를 들어 다음과 같이 과감하게 hoge?.fuga?.piyo와의 유니온형을 시도한다.
    React의 Component로 정의하면 편리합니다.
    type Props = {
      width: number | undefined
      height: number | undefined
    }
    
    export const Rectangle = (props: Props) => {
      const { width = 100, height = 100 } = props
    
      return // 以下略...
    }
    
    undefinedwidth?: number의 차이는 이 속성이 존재하지 않도록 허용하는지 여부에 있다.
    상기 Component는 호출 측에서 width: number | undefinedwidth의 속성을 지정해야 한다.
    <Rectangle /> {/* width と height がないというエラーになる */}
    
    이것은 폼 입력이 없을 때도 기본값을 사용해서 이동할 때 편리하기를 바랍니다.
    const App = () => {
      const [size, setSize] = useState<{width: number, height: number} | null>(null);
      
      return ( 
      <>
        <input onChange={e => {
          const values = e.target.value.split(",")
          setSize({ width: parseInt(values[0]), height: parseInt(values[1]) })
        }} />
        <Rectangle width={size?.width} height={size?.height} />
      </>);
    };
    
    응, 솔직히
    <Rectangle width={size?.width ?? 100} height={size?.height ?? 100} />
    
    처럼null합체산자height와 조합하여 사용하면 이해하기 쉽고 묘사도 간결하다.
    (상기 예에서parseInt는 NaN을 반환할 가능성이 있지만 NaN은 사용하더라도?? 보통 왼쪽 값으로 되돌아오기 때문에 안전성은 어느 것을 사용하든지 변하지 않는다)

    끝맺다


    이번에는 기억하기 쉽고 직관적으로 쓴 성어를 소개했다.
    아마도 CondiionalTypes 같은 흑마술을 쓰면 더 복잡한 일이 생길 수 있겠지만, 솔직히 이 점에 쓰지 않고 아무리 노력해도 자바스크립트 부분으로 전환할 수 있다면 앱은 평생 완성되지 않을 것이고, 개인적으로 너무 많이 하면 성가가 좋지 않을 것이라고 생각한다.
    Template Literals는 다양한 방법을 적용할 수 있기 때문에 생각나는 대로 기사를 쓴다.
    그럼 편한 타입 스크립트 생활을 하세요.

    좋은 웹페이지 즐겨찾기