[Typescript] 이펙티브 타입스크립트 정리(아이템1 ~ 8)

1장. 타입스크립트 알아보기

아이템1 : 타입스크립트와 자바스크립트 관계 이해하기

타입스크립트는 자바스크립트의 슈퍼셋이다.

기존에 존재하는 자바스크립트 코드를 타입스클비트로 마이그레이션 하는 것은 기존 코드에서 일부분에만 타입스크립트 적용이 가능하기 때문에 엄청난 이점이라고 할 수 있다.

모든 자바스크립트 프로그램은 타입스크립트이지만, 모든 타입스크립트 프로그램이 자바스크립트는 아니다. 타입스크립트에는 타입을 명시하는 추가적인 문법들이 존재한다.

타입 시스템의 목표 중 하나는 런타임에 오류를 미리 찾아내는 것인데, 타입스크립트가 ‘정적’ 타입 시스템이라는 것이 이런 특징 때문이다. 타입스크립트는 타입 구문 없이도 오류를 잡아낼 수 있지만, 적절한 타입을 추가하면 더 많은 오류를 찾아낼 수 있다.

요약

  • 타입스크립트는 자바스크립트의 상위집합이다.
  • 타입스크립트는 자바스크립트 런타임 동작을 모델링하는 타입 시스템을 가지고 있기 때문에 런타임 오류를 발생시키는 코드를 찾아내려고 한다. 그러나 모든 오류를 찾아내는 것은 아니다. 타입 체커를 통과하면서 런타임 오류를 발생시키는 코드도 충분히 존재할 수 있다.
  • 타입스크립트 타입 시스템은 전반적으로 자바스크립트 동작을 모델링한다. 잘못된 매개변수 개수로 호출하는 경우처럼, 자바스크립트에서는 허용되나 타입스크립트에서 문제가 되는 경우도 있다.

아이템2 : 타입스크립트 설정 이해하기

tsconfig.json은 타입스크립트 설정 내용이 포함되어있는 설정 파일이다. 이 중에 noImplicitAnystrictNullChecks 옵션을 제대로 이해해야 설정을 제대로 사용할 수 있다.

noImplicitAny는 변수들이 미리 정의된 타입을 가져야하는지 여부이다. 타입스크립트는 타입 정보를 가질 때 가장 효과적이기 때문에 되도록이면, noImplicitAny를 설정하는 것이 좋다. 게다가 문제를 발견하기 수월해지고, 코드의 가독성이 좋아지며, 개발자의 생산성이 향상되는 효과를 기대할 수 있다.

strictNullChecksnullundefined가 모든 타입에서 허용되는지 확인하는 설정이다. 이 옵션은 null이나 undefined과 관련된 오류를 잡아 내는 데 많은 도움이 되지만, 코드 작성이 어려워진다는 단점이 있다. 그래서 자바스크립트 마이그레이션과 같은 과정이라면 사용하지 않는 편이 나을 수 있다.

요약

  • 타입스크립트 컴파일러는 언어의 핵심 요소에 영향을 미치는 몇 가지 설정을 포함한다.
  • 타입스크립트 설정은 커맨드 라인을 이용하기보다 tsconfig.json을 사용하는 것이 낫다.
  • 자바스크립트 프로젝트를 타입스크립트로 전환하는게 아니라면 noImplicitAny를 설정하는 것이 좋다.
  • undefined는 객체가 아닙니다와 같은 오류를 방지하려면 strictNullChecks를 설정하는 것이 좋다.

아이템 3 : 코드 생성과 타입이 관계없음을 이해하기

타입스크립트의 컴파일러는 두 가지 역할을 한다.

  1. 최신 타입스크립트/자바스크립트를 브라우저에서 동작할 수 있도록 구버전 자바스크립트로 트랜스파일(transpile)함.
  2. 코드의 타입 오류를 체크함

중요한 것은 위의 두 가지 역할이 완벽히 독립적이다. 위 두가지 역할에서 볼 때 타입스크립트의 가능한 부분과 불가능한 부분을 추론할 수 있다

  • 타입 오류가 있는 코드도 컴파일 가능하다.

컴파일은 타입 체크와 독립적으로 동작하기 때문에 타입 오류가 있는 코드도 컴파일 가능하다. 타입스크립트에서는 문제가 될 부분을 알려주지만, 빌드를 멈추지는 않는다. 타입스크립트 컴파일러는 유효한 자바스크립트이면 컴파일을 해낸다. 오류가 뜨면 컴파일 오류가 아니고 타입 체크 오류라고 할 수 있다.

  • 런타임에는 타입 체크가 불가능하다.

런타임에 타입 체크가 불가능하지만 타입 정보를 유지하는 방법으로 태그된 유니온(tagged union)이 있다. 해당 기법을 사용하면 런타임에 타입 정보를 쉽게 유지할 수 있다. 예시는 아래와 같다.

interface Square {
  kind: 'square'
  width: number
}
interface Rectangle {
  kind: 'rectangle'
  height: number
  width: number
}
type Shape = Square | Rectangle
  • 타입 연산은 런타임에 영향을 주지 않는다.
function asNumber(val: number | string): number {
  return val as number
}

as number와 같은 타입 연산은 런타임 동작에는 아무런 영향을 미치지 않는다. 그렇기 때문에 값을 정제하기 위해서는 런타임의 타입을 체크해야 하고 자바스크립트 연산을 통해 변환을 수행해야 한다.

  • 런타임 타입은 선언된 타입과 다를 수 있다.

런타임 타입과 선언된 타입이 맞지 않을 수 있다. 선언된 타입이 언제든지 달라질 수 있다는 것을 명심해야 한다.

  • 타입스크립트 타입으로는 함수를 오버로드 할 수 없다.

타입스크립트에서는 타입과 런타임의 동장이 무관하기 때문에, 함수 오버로딩은 불가능하다.

function add(a: number, b: number) {
  return a + b
}
// ~~~ Duplicate function implementation
function add(a: string, b: string) {
  return a + b
}
// ~~~ Duplicate function implementation

하나의 함수에 대해 여러 개의 선언문을 작성할 수 있지만, 구현체(implementation)은 하나 뿐이다.

  • 타입스크립트 타입은 런타임 성능에 영향을 주지 않는다.

타입과 타입 연산자는 자바스크립트 변환 시점에 제거되기 때문에, 런타임 성능에 아무 영향을 주지 않는다. 런타임 오버헤드가 없는 대신 빌드타임 오버헤드가 있다.

요약

  • 코드 생성은 타입 시스템과 무관하다. 타입은 런타임 동작이나 성능에 영향을 주지 않는다.
  • 타입 오류가 존재해도 코드 생성(컴파일)은 가능하다.
  • 타입스크립트 타입은 런타임에 사용할 수 없다. 런타임에 타입을 지정하려면 타입정보 유지를 위한 별도 방법이 필요하다. 일반적으로는 태그된 유니온과 속성 체크 방법을 사용한다.

아이템 4 : 구조적 타이핑에 익숙해지기

자바스크립트는 덕타이핑 기반이다. 타입 체커의 타입에 대한 이해도가 사람과 조금 다르기 때문에 가끔 예상치 못한 결과가 나오기도 한다.

덕타이핑은 객체가 어떤 타입에 부합하는 변수와 메서드를 가질 경우 객체를 해당 타입에 속하는 것으로 간주하는 방식이다.

  • 자바스크립트가 덕 타이핑 기반이고 타입스크립트가 이를 모델링하기 위해 구조적 타이핑을 사용해야하는 것을 이해해야 한다. 타입은 봉인 되어있지 않다.
  • 클래스 역시 구조적 타이핑 규칙을 따른다는 것을 명심해야 한다. 클래스의 인스턴스가 예상과 다를 수 있다.
  • 구조적 타이핑을 사용하면 유닛 테스팅을 손쉽게 할 수 있다.

아이템 5 : any 타입 지양하기

타입스크립트의 타입 시스템은 코드에 타입을 조금씩 추가할 수 있기 때문에 점진적이고, 언제든지 타입 체커를 해제할 수 있기 때문에 선택적이다. 이 때, any가 핵심 역할을 하는 타입이다. any를 사용하면 타입 선언을 안해도되고, 에러를 간단히 해결해 줄 수 있기 때문에 많이 쓰고싶을 수 있지만, 특별한 경우를 제외하고는 any를 사용하면 타입스크립트의 장점을 누릴 수 없게 된다.

  • any 타입에는 타입 안전성이 없다.
  • any는 함수 시그니처를 무시해버린다.
  • any 타입에는 언어 서비스가 적용되지 않는다. (자동완성, 메서드 등)
  • any 타입은 코드 리팩터링 때 버그를 감춘다.
  • any는 타입 설계를 감춰버린다.
  • any는 타입시스템의 신뢰도를 떨어뜨린다.

요약

  • any 타입을 사용하면 타입 체커와 타입스크립트 언어 서비스를 무력화시켜버린다. any 타입은 진짜 문제점을 감추고, 개발 경험을 나쁘게하고, 신뢰도를 떨어뜨리기 때문에 최대한 사용을 피하도록하자.

2장. 타입스크립트의 타입 시스템

아이템 6 : 편집기를 사용하여 타입 시스템 탐색하기

요약

  • IDE에서 타입스크립트 언어 서비스를 적극 활용해야 한다.
  • IDE를 사용하면 어떻게 타입 시스템이 동작하는지, 그리고 어떻게 타입을 추론하는지 개념을 잡을 수 있다.
  • 타입스크립트가 어떻게 모델링하는지 알기 위해 타입 선언 파일을 찾아보는 방법을 터득해야 한다.

아이템 7 : 타입이 값들의 집합이라고 생각하기

가장 작은 집합은 아무 값도 포함하지 않는 공집합이며, 타입스크립트에서는 never 타입이다. 그다음 작은 집합은 한 가지 값만 포함하는 리터럴 타입이다. 두 개 혹은 세 개로 묶을 땐, 유니온(union)타입을 사용한다.

type A = 'a' // 리터럴 타입
type AB = 'a' | 'b' // 유니온 타입
interface Identified {
  id: string
}
interface Person {
  name: string
}
interface Lifespan {
  birth: Date
  death?: Date
}
type PersonSpan = Person & Lifespan
const ps: PersonSpan = {
  name: 'Alan Turing',
  birth: new Date('1912/06/23'),
  death: new Date('1954/06/07'),
} // OK

& 연산자는 두 타입의 인터섹션이고, Person과 LIfespan을 둘 다 가지는 값은 인터섹션 타입에 속하게 된다. 다시 말해, 각 타입 내의 속성을 모두 포함하는 것이 일반적인 규칙이다.

keyof (A&B) = (keyof A) | (keyof B)
keyof (A|B) = (keyof A) & (keyof B)

조금 더 효과적으로 선언하는 방법은 아래와 같이 extends 키워드를 하는 것이다. 이것은 ‘~의 부분집합’ 이라고 이해하면 된다.

interface Person {
  name: string
}
interface PersonSpan extends Person {
  birth: Date
  death?: Date
}

요약

  • 타입을 값의 집합으로 생각하면 이해하기 편하다.
  • 타입스크립트 타입은 엄격한 상속 관계가 아닌 겹쳐지는 집합으로 표현된다.
  • 한 객체의 추가적인 속성이 타입 선언에 언급되지 않아도 그 타입에 속할 수 있다.
  • 타입 연산은 집합의 범위에 적용된다. A와 B의 인터섹션은 A의 범위와 B의 범위의 인터섹션이다. 객체 타입에서는 A & B인 값이 A와 B의 속성을 모두 가짐을 의미한다.
  • ‘A는 B를 상속’, ‘A는 B에 할당 가능’, ‘A는 B의 서브타입’은 모두 ‘A는 B의 부분 집합’과 같은 의미이다.

아이템 8 : 타입 공간과 값 공간의 심벌 구분하기

타입스크립트의 심벌(symbol)은 타입 공간이나 값 공간 중의 한 곳에 존재한다.

interface Cylinder {
  radius: number
  height: number
}

const Cylinder = (radius: number, height: number) => ({ radius, height })
function calculateVolume(shape: unknown) {
  if (shape instanceof Cylinder) {
    shape.radius
    // ~~~~~~ Property 'radius' does not exist on type '{}'
  }
}

instanceof는 자바스크립트의 런타임 연산자이고, 값에 대하여 연산하기 때문에 타입이 아닌 함수를 참조하여서 오류가 발생한 것이다. 한 심벌이 타입인지 값인지는 언뜻봐서는 알 수 없고, 문맥을 살펴봐야 한다.

classenum은 상황에 따라 타입과 값 모두 가능하다. 클래스가 타입으로 쓰일 때는 형태(속성과 메서드)가 쓰이고, 값으로 쓰일 때는 생성자가 사용된다. typeof같은 경우는 타입 공간에서는 보다 큰 타입의 일부분으로 사용 가능하고, 값의 관점에서는 런타임의 typeof 연산자가 된다.

요약

  • 타입스크립트 코드를 읽을 때 타입인지 값인지 구분하는 방법을 터득해야 한다.
  • 모든 값은 타입을 가지지만, 타입은 값을 가지지 않는다. typeinterface는 타입 공간에만 존재한다.
  • classenum은 타입과 값 두 가지로 사용될 수 있다.
  • “foo”는 문자열 리터럴이거나, 문자열 리터럴 타입일 수 있다.
  • typeof,this 그리고 많은 다른 연산자들과 키워드들은 타입 공간과 값 공간에서 다른 목적으로 사용될 수 있다.

출처


Written by@[Ykss]
고이게 두지 않고 흘려보내는 개발자가 되자.

GitHubInstagramLinkedIn