해결된 질문
작성
·
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 언어에 비해 굉장히 자유도가 높은데, 그만큼 아직 구현이 안된 것이 종종 있습니다.
안녕하세요, 답변 감사합니다.
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의 유튜브 영상에서 이런 말을 했던 것이 기억납니다. 정확한 표현은 아니지만, 대략 이런 내용이었습니다.
"우리가 타입스크립트를 개발한 지 몇 년이 지났지만, 여전히 새로운 문제를 직면하고 있다."
제 좋지 않은 기억력에 의존한 의역이라 정확한 표현은 아닐 수 있지만, 아무튼 타입 이론이 결코 단순하지 않은 복잡한 주제라는 점을 다시금 느끼게 되네요.