- Published on
타입스크립트에서 조심해야할 습관
- Author
- Name
- yceffort
Table of Contents
any
타입스크립트에서 얼마나 타입을 잘 지키는지는, any
를 얼마나 사용하느냐에 달려 있다고 봐도 될 것 같다. 그러나 불가피하게 any
가 사용되는 경우도 있다. JSON.parse
가 그 중 하나다. 이 메소드는, 리턴타입을 추론 할 수 없기 때문에 any
로 리턴을 한다.
JSON.parse('{hello: world}')
// (method) JSON.parse(text: string, reviver?: ((this: any, key: string, value: any) => any) | undefined): any
any
의 유혹은 강력하다. 그냥 뭐든지 넘겨주기 때문이다.
function testNumber(num: number) {
return num + 1
}
const num: any = 'eleven' // any 라서 number만 넘길 수 있었는데 무시되었다.
testNumber(num)
따라서 any
의 사용은 최소화 하는 것이 좋다. 타입을 아직 알 수 없을 때는, any
대신 unknown
을 쓰는게 좋다.
Type Assertion
type assertion
은 엄밀히 말해서 cast
와는 다르다. 타입스크립트에서 type assertion
이란 x as number
와 같은 형태를 의미한다. 타입이 있는 다른언어 (C, Java등) 에서 cast
는 런타임에 영향을 미친다. 예를 들어, (int)f
가 있다면 float이 런타임 중에 int로 바뀌게 된다. 그러나 타입스크립트는 런타임시에 타입을 제거하기 때문에 실제로 아무런 영향을 미치지 않는다.
const strOrNum = Math.random() ? '42' : 42
const num = strOrNum as number
을 컴파일하면 별 차이가 없다.
'use strict'
const strOrNum = Math.random() ? '42' : 42
const num = strOrNum
따라서 type assertion
이란 타입으로 캐스팅을 하는게 아니고, 그냥 그게 이 타입이라고 주장하는 것이다. (그래서 assertion
이기도 하구)
function testNumber(num: number) {
return num + 1
}
const num = Math.random() ? 'eleven' : 11
testNumber(num as number) // 반반쯤의 확률로 에러가 난다.
이러한 as
의 사용은 api 호출에서도 자주 보인다.
const response = await fetch('/api/user')
const result = (await response.json()) as UserInterface
이러한 경우에는, 타입 가드를 써서 막는 것이 좋다.
function isUser(data: unknown): data is UserInterface {
return data && typeof data === 'objet' && 'name' in data // ...
}
const response = await fetch('/api/user')
const result = await response.json()
if (!isUser(result)) {
throw new Error(`${result}는 UserInterface가 아니예욧`)
}
조금더 빡세게(?) 타입가드를 하고 싶다면, Zod와 같은 도구를 사용해보는 것도 좋다. 혹은 typescript-json-schema로 JSON schema에서 타입을 뽑아낸 다음에 검사하는 방법도 있을 수 있다.
객체와 배열 lookup
타입스크립트는 배열을 참조할 때 별다른 처리를 하지 않기 때문에, 에러가 날 수 있다.
const l = [1, 2, 3]
const item = l[3]
item + 1 // error 지만, 컴파일시에는 모른다.
const user: { [key: string]: string } = { name: 'kyc' }
user.age + 1 // error 지만, 컴파일 시에는 모른다.
왜 타입스크립트가 가만 뒀을까? 아마도 이러한 상황은 매우 자주 있는 일이고, 이것이 유효한지 체크하는 것이 꽤 어려운일이어서 그런걸수도 있다. 근데, 이를 체크하는 방법이 있다. 바로 noUncheckedIndexedAccess다.
const l = [1, 2, 3]
const item1 = l[3]
const item2 = l[2]
item1 + 1 // Object is possibly 'undefined'.(2532)
item2 + 1 // 근데 문제는 이거까지 에러가 난다는 거다.
l.map((item) => item + 1) // 이런건 괜찮다.
위에서 보이는 것 처럼, noUncheckedIndexedAccess
는 적당히 경고를 날려주는 장점도 있지만, 그다지 똑똑하지 않다는 단점도 있다.
결론적으로, 객체나 리스트를 lookup 할때는 undefined가 있을 가능성에 대해서 염두해 두어야 한다.
부정확한 타입 정의
자바스크립트의 라이브러리에 타입선언을 집어넣는 것은 일종의 거대한 type assertion
이다. 그들의 라이브러리가 이렇게 정적으로 모델링 했다고 주장하는 거지만, 이를 보장하는 것은 아무것도 없다. (물론 이는 라이브러리가 타입스크립트로 작성되지 않았다는 것에 한해서다. 타입스크립트로 작성되지 않고, 타입만 있다면 이런일이 발생할 수 있다.)
예를 들면 2년째 고쳐지지 않는 잘못된 타이핑 이라던가...
이런 문제를 해결하는 가장 좋은 방법은, 직접 버그를 고치는 것이다. 직접 DefinitelyTyped에 쳐들어가서 고치면 된다. 이게 좀 부담되는 나와 같은 ISTJ 들에겐 augmentation 이나, 최악의 옵션으로 type assertion을 활용하는 방법도 있다.
물론, 몇몇 함수는 이렇게 타입을 선언하기가 굉장히 어렵다는 것도 이해해줘야 한다. String.prototype.replace의 파라미터를 잠깐 보자. Object.assign의 경우엔, 피치못할 사정으로 인해 잘못된 타이핑이 된 경우도 있었다.
variance and arrays
typescript github의 이 이슈를 살펴보자.
function addDogOrCat(arr: Animal[]) {
arr.push(Math.random() > 0.5 ? new Dog() : new Cat())
}
const z: Cat[] = [new Cat()]
addDogOrCat(z) // 개가 들어갈 수도 있다.
이러한 짓을 방지하려면 어떻게 해야할까? readonly
를 쓰는 방법이 있을 수 있다.
function addDogOrCat(arr: readonly Animal[]) {
arr.push(Math.random() > 0.5 ? new Dog() : new Cat())
// Property 'push' does not exist on type 'readonly Animal[]'.
}
아니면, push
대신 이렇게 하거나.
function dogOrCat(): Animal {
return Math.random() > 0.5 ? new Dog() : new Cat()
}
const z: Cat[] = [new Cat(), dogOrCat()]
함수 호출에 따른 부작용
interface UserInterface {
name: string
age?: number
}
function userProcessor(user: UserInterface, processor: (user: UserInterface) => void) {
if (user.age) {
processor(user)
document.body.innerHTML = `${user.age + 1}`
}
}
만약 processor
에서 이런짓을 하면 어떻게 될까?
userProcessor({ name: 'kyc', age: 15 }, (u) => delete u.age)
// 타입 체크는 성공하지만, 에러가 날거다.
물론 만일을 위해서 if
처리가 추가되었지만, processor
에서 이를 무효화 했다. 타입스크립트는 저 함수에서 무슨짓을 할지 모르기 때문에, 어찌보면 당연한 것이다.
자바스크립트에서 파라미터를 조작하는 일은 흔치 않고 또한 안티패턴이기 때문에, 타입스크립트에서는 이러한 동작을 허용해 뒀을 것이다.
이를 수정해두는 방법은, 역시 readonly
를 사용하는 것이 있다.
function userProcessor(
user: UserInterface,
processor: (user: Readonly<UserInterface>) => void,
) {
if (user.age) {
processor(user)
document.body.innerHTML = `${user.age + 1}`
}
}
userProcessor({ name: 'kyc', age: 15 }, (u) => delete u.age)
// The operand of a 'delete' operator cannot be a read-only property.
Readonly는 얕은 비교만 하기 때문에, ts-essentials를 사용해야 깊은 비교를 할 수 있다.
당연하게도, 가장 좋은 방법은 객체의 처리를 객체가 정의된 이후에 하는 것이다.
function processFact(
user: UserInterface,
processor: (user: UserInterface) => void,
) {
const { age } = user
if (age) {
processor(user)
document.body.innerHTML = `${age + 1}` // safe
}
}