인프런 영문 브랜드 로고
인프런 영문 브랜드 로고

인프런 커뮤니티 질문&답변

Hyeongjong Kim님의 프로필 이미지

작성한 질문수

실전 연습으로 익히는 고급 타입스크립트 기술

Filter 함수 타입 제대로 지정하기

제네릭과 사용자 정의 타입 가드에 관한 질문

해결된 질문

작성

·

82

1

안녕하세요 타입스크립트 강의 잘 듣고 있습니다. 좋은 강의 감사합니다. 하지만 아직 제 머리로는 이해가 되지 않는 부분이 있어 질문을 드리는데요.

여기 제가 구현하고자 하는DatePicker라는 클래스가 있고 이 클래스를 하나의 날짜를 선택하는 'single'타입의 DatePicker와 기간을 선택하는 'range' 타입의 DatePicker로 사용 가능하도록 만드려고 합니다.

type SingleValue = Date | null
type RangeValue = SingleValue[] & { length: 2 }
type DatePickerValue<T extends 'single' | 'range'> = {
  'single': SingleValue,
  'range': RangeValue
}[T]

class DatePicker<T extends 'single' | 'range'> {
  type:T
  value: DatePickerValue<T>

  constructor(type:T, initialValue:DatePickerValue<T>) {
    this.type = type
    this.value = initialValue
  }

  updateValue(date:Date) {
    // update value...
  }
}

const singeDatePicker = new DatePicker('single', new Date('2025-01-01'))
singeDatePicker.value = new Date('2025-01-15') // (property) DatePicker<"single">.value: SingleValue


const rangeDatePicker = new DatePicker('range', [new Date('2025-01-01'), new Date('2025-01-05')])
rangeDatePicker.value = [new Date('2025-01-03'), new Date('2025-01-10')] // (property) DatePicker<"range">.value: RangeValue

이렇게 DatePicker클래스의 type property의 제네릭을 통해 value의 타입도 잘 추론이 되는데요. 문제는 클래스 내부에서 타입을 좁히고 싶은데 잘 되지 않습니다.

아래처럼 updateValue 메소드를 DatePicker클래스에 추가하여 외부 달력 클릭시 날짜 객체를 인자로 넘겨 클래스의 value를 업데이트 해주기 위한 로직을 구현하려고 하는데, 클래스 내부에서는 사용자 정의 타입 가드를 사용해도 value 의 타입이 좁혀지지가 않습니다.

  updateValue(date:Date) {
    const isSingle = ():this is DatePicker<'single'> => this.type === 'single'
    const isRange = ():this is DatePicker<'range'> => this.type === 'range'

    if(isSingle()) {
      this.value = date
      // Type 'Date' is not assignable to type 'DatePickerValue<T>'.
      // Type 'Date' is not assignable to type 'Date & SingleValue[] & { length: 2; }'.
      // Type 'Date' is missing the following properties from type 'SingleValue[]': length, pop, push, concat, and 26 more.(2322)
      // (property) DatePicker<T extends "single" | "range">.value: DatePickerValue<T>
      return;
    }

    if(isRange()) {
      this.value = [date, null]
      // Type '[Date, null]' is not assignable to type 'DatePickerValue<T>'.
      // Type '[Date, null]' is not assignable to type 'Date & SingleValue[] & { length: 2; }'.
      // Type '[Date, null]' is not assignable to type 'Date'.(2322)
      // (property) DatePicker<T extends "single" | "range">.value: DatePickerValue<T>
      return;
    }
  }

제가 궁금한 것은 이렇게 클래스 내부에서 value: DatePickerValue<T>와 같은 조건부 타입을 사용자 정의 타입 가드로 추론을 하는 것이 가능한지 알고 싶습니다. 만약 안된다면 이유에 대해 설명 해주시면 감사하겠습니다.

아니면 제 구현 방식의 문제가 있다면 지적해 주시면 많은 도움이 될 것 같습니다.

감사합니다.

답변 1

1

애프터캠프님의 프로필 이미지
애프터캠프
지식공유자

안녕하세요.

우선 제가 생각한 답안을 보여드리면

type SingleValue = Date | null
type RangeValue = [SingleValue, SingleValue]
type DatePicker =
  | { type: 'single'; value: SingleValue }
  | { type: 'range'; value: RangeValue };

class DatePickerKlass {
  picker: DatePicker;

  constructor(picker: DatePicker) {
    this.picker = picker;
  }

  updateValue(date: Date) {
    if (this.picker.type === 'single') {
      this.picker.value = date;
    } else if (this.picker.type === 'range') {
      this.picker.value = [date, null];
    }
  }
}

작성해주신 답안 보다 좀 더 간단하게 만들 수 있지 않을까 해서 저 같은 경우엔 discriminated union 으로 문제 풀어봤습니다.


  updateValue(date:Date) {
    const isSingle = ():this is DatePicker<'single'> => this.type === 'single'
    if(isSingle()) {
      this.value = date
      return;
    }
  }

여기서 타입이 좁혀지지 않는 이유는 타입스크립트의 특성인데요. T에 타입을 제한해도 결국 T는 'placeholder'이기 때문에 특정 타입으로 좁혀지지 않습니다.

저도 실무에서 사용하다보면, 생각대로 안되는 게 참 많은데 이럴 땐 "아! 이건 아직 타입스크립트 개발하시는 분이 구현을 안해놨구나..." 라고 생각합니다. 버전이 높아지면 이런 문제도 해결되는 경우가 있거든요.

타입스크립트가 다른 statically-typed 언어에 비해 굉장히 자유도가 높은데, 그만큼 아직 구현이 안된 것이 종종 있습니다.

 

 

Hyeongjong Kim님의 프로필 이미지

안녕하세요, 답변 감사합니다.

T가 placeholder이기 때문에 타입이 좁혀지지 않는 것은 생각해보면 당연한 것 같네요. 말씀해주신 것처럼 discriminated union을 사용하는 것도 하나의 해결 방법이 될 수 있을 것 같습니다. 유용한 내용 공유해 주셔서 감사합니다.

그리고 타입 가드와 관련하여 제 질문에 잘못된 부분이 있어, 다른 분들이 헷갈리지 않도록 정정하겠습니다.

클래스에서 this의 타입을 좁히기 위해서는 클래스의 메소드로 타입 가드를 구현해야 합니다. 하지만 저는 updateValue 내부에서 타입 가드를 구현했기 때문에 타입이 전혀 좁혀지지 않았습니다(DatePickerValue<T>).

그러나 아래와 같이 타입 가드를 클래스의 메소드로 구현하면, 여전히 문제가 존재하긴 하지만, 그래도 타입이 intersection(DatePickerValue<T> & SingleValue)으로 좁혀지는 모습을 확인할 수 있었습니다.

class DatePicker<T extends 'single' | 'range'> {
  type:T
  value: DatePickerValue<T>

  constructor(type:T, initialValue:DatePickerValue<T>) {
    this.type = type
    this.value = initialValue
  }

  isSingle(): this is DatePicker<'single'> {
    return this.type === 'single'
  }

  updateValue(date:Date) {
    if(this.isSingle()) {
      this.value = date 
      // Error: Type 'Date' is not assignable to type 'DatePickerValue<T> & SingleValue'.
    }
}

얼마 전, 타입스크립트 코어 개발자인 Anders Hejlsberg의 유튜브 영상에서 이런 말을 했던 것이 기억납니다. 정확한 표현은 아니지만, 대략 이런 내용이었습니다.
"우리가 타입스크립트를 개발한 지 몇 년이 지났지만, 여전히 새로운 문제를 직면하고 있다."

제 좋지 않은 기억력에 의존한 의역이라 정확한 표현은 아닐 수 있지만, 아무튼 타입 이론이 결코 단순하지 않은 복잡한 주제라는 점을 다시금 느끼게 되네요.