타입스크립트에서 숫자타입에 대해서 정수로만 입력 가능하게, 또는 양수로만 입력 가능하게 하면 얼마나 좋을까 라는 생각은 다들 했겠지만, 불가능하다고 알고있었다.
그런데 트위터를 하다보니 그걸 구현하신 분이 있었다.
일단 당장은 크게 쓰일것 같지 않지만 너무 신기하니 써놔야겠다.
타입스크립트 4.1 에서 추가된 기능인 Template Literal Type 은 ES6+ 의 Template Literal 문법에서 유래된 타입 정의 문법이다. 요는 특정 형식의 문자열 패턴을 타입으로 강제할수 있다는 것.
export type DisplayName = `${string} (${number})`
const name1: DisplayName = '김철수' // Error
const name2: DisplayName = '김철수 (59)' // OK
TS개발진들이 의도한건지 모르겠지만, Template Literal Type 을 제네릭 그리고 infer 키워드와 조합하면 별의별 말도 안되는 일들을 할수 있게 된다.
내가 보았던 트위터에서 보았던 코드도 비슷한 방법으로 구현했다. 문자열로 치환했을때 소수점 존재 여부로 정수인지 유리수인지 체크하는 것이다.
export type Int<T> = T extends number
? `${T}` extends `${infer U}.${infer R}`
? never
: T
: never
const asssertInt = <T>(num: Int<T>) => num
asssertInt(123)
asssertInt(123.123) // Error
asssertInt(-123)
asssertInt(-123.123) // Error
같은 방법으로 양수만 허용하는 타입도 만들어볼수 있다.
export type PositiveNumber<T> = T extends number
? `${T}` extends `-${infer U}`
? never
: T
: never
const assertPositive = <T>(num: PositiveNumber<T>) => num
assertPositive(123)
assertPositive(123.123)
assertPositive(-123) // Error
assertPositive(-123.123) // Error
그리고 그 둘을 intersection 시켜서 양의 정수만 받도록 할 수도 있다.
export type Int<T> = T extends number
? `${T}` extends `${infer U}.${infer R}`
? never
: T
: never
export type PositiveNumber<T> = T extends number
? `${T}` extends `-${infer U}`
? never
: T
: never
export type PositiveInt<T> = Int<T> & PositiveNumber<T>
const assertPositiveInt = <T>(num: PositiveInt<T>) => num
assertPositiveInt(123)
assertPositiveInt(123.123) // Error
assertPositiveInt(-123) // Error
assertPositiveInt(-123.123) // Error
신기하긴 한데 실제 개발과정에서 쓰지는 못할것 같다. 타알못이라 정확하게는 모르겠지만 순수한 리터럴 숫자에서만 타입검증이 유효하다. 계산식이 포함되면 제대로 검증을 못한다.
asssertInt(2.5) // Error
asssertInt(1 + 1.5) // OK
게다가 입력받는 값이 정수인지 아닌지 판별하는 상황의 대부분은 사용자의 입력 또는 요청을 받을 때이다. 타입스크립트는 빌드타임 이전의 타입체킹만 가능하기 때문에 타입검증 아무리 해봐야 사용자 입력에 대한 처리는 안된다.
결국은 밸리데이션 로직이 붙어야 한다는 슬픈 이야기...