똑같은 삽질은 2번 하지 말자
Effective Typescript vol.2 (타입스크립트의 타입 시스템 Item 6 ~ 18 ) 본문
개요
이펙티브 타입스크립트라는 책을 읽고 장별(각각의 장안에 아이템별)로 학습한 내용 정리하기
이 책의 목표: 독자에게 타입스크립트나 자바스크립트를 가르치는 것이 아니라, 초급자나 중급자가 전문가로 발전할 수 있게 돕는 것
프로그래밍에서 어떤 방법을 가르치는게 아니라 어떤 방법을 사용할 때 왜 그래야 하는지를 알려준다.
2장 타입스크립트의 타입 시스템
Item6. 편집기를 사용하여 타입 시스템 탐색하기
우리가 typescript라는 패키지를 설치하면 두가지를 할 수 있는데,
- 타입스크립트 파일을 자바스크립트로 컴파일하는 tsc를 쓸 수 있다.
- 단독으로 실행할 수 있는 타입스크립트 서버(tsserver)를 쓸 수 있다.
여기서 타입스크립트 서버란 우리가 사용하는 편집기(VSCode)에서 코드 자동완성, 명세, 검색, 리팩터링등등 언어서비스를 제공하는 서버 인 것이다. 즉, 우리는 편집기를 이용해 이 타입스크립트 서버가 제공하는 언어서비스를 적극적으로 활용하면 할수록 타입스크립트의 은혜를 더더욱 많이 받을 수 있다.
Item7. 타입이 값들의 집합이라고 생각하기
- 타입을 값의 집합이라 생각하자. 이 집합은 유한하거나 무한하다.
- 타입은 엄격한 상속 관계가 아니라 겹쳐지는 집합으로 표현된다. 두 타입은 서로 서브타입이 아니면서도 겹쳐질 수 있다.
- 타입에 없는 속성이 추가되더라도 타입체커에 걸리지 않는다 (구조적 타입, 덕 타이핑)
- A는 B를 상속 == A는 B에 할당 가능 == A는 B의 서브타입 == A는 B의 부분 집합
인터섹션과 유니온..헷갈리지 말자 이하의 페이지를 참고하면 조금 더 이해하기 쉽다.
https://joshua1988.github.io/ts/guide/operator.html#intersection-type
연산자를 이용한 타입 정의 | 타입스크립트 핸드북
Union Type 유니온 타입(Union Type)이란 자바스크립트의 OR 연산자(||)와 같이 A이거나 B이다 라는 의미의 타입입니다. 아래 코드를 보겠습니다. 위 함수의 파라미터 text에는 문자열 타입이나 숫자 타입
joshua1988.github.io
Item8. 타입 공간과 값 공간의 심벌 구분하기
타입스크립트의 심벌은 타입 공간이나 값 공간 중의 한 곳에 존재한다.
(symbol ? 간단하게 말하면 이름은 같은데 다른 공간에 존재할 수 있는 자바스크립트의 타입)
Symbol - JavaScript | MDN
Symbol() 함수는 심볼(symbol) 형식의 값을 반환하는데, 이 심볼은 내장 객체(built-in objects)의 여러 멤버를 가리키는 정적 프로퍼티와 전역 심볼 레지스트리(global symbol registry)를 가리키는 정적 메서드
developer.mozilla.org
(여담, JavaScript는 총 6개의 원시 타입(number, string, boolean, undefined, object, function)의 런타임 타입만이 존재 null이라는 타입이 존재하냐 안하냐에 관해서는 이하의 포스팅을 참조하면 좋을꺼 같다. )
https://curryyou.tistory.com/183
[JS] 자바스크립트 null은 객체? 기본 타입! (typeof null)
# 자바스크립트 null? 자바스크립트의 null은 '의도적으로 값이 없음'을 명시하기 위한 기본 데이터 타입이다. 타입도 null이며, 값도 null인 Primitive Type이다. 즉, null은 객체가 아니다!(기본 타입이다!
curryyou.tistory.com
예를 들어보자.
위에는 타입으로써의.Cylinder이고 밑에는 값으로써의 Cylinder이다.
이는 잘못된 코드가 아니며 두개의 이름은 전혀 다른 공간에 존재한다. 값은 값으로써 타입은 타입으로써
그래서 타입스크립트를 사용할 때 이하의 주의사항을 숙지해두자
- 위의 예시처럼 타입과 값을 구분을 잘해야 한다. 이로 인해 타입체크가 잘 안되거나 런타임 에러도 야기할 수 있다.
- 모든 값은 타입을 가지지만, 타입은 값을 가지지 않는다.
- class나 enum은 타입과 값 두가지로 사용될 수 있다.
- "특정한 문자열"은 단순히 string타입은 문자열 리터럴이거나, 문자열 리터럴 타입일 수도 있다.
- typeof, this등등 다른 많은 연산자들과 키워드들은 타입 공간과 값 공간에서 다른 목적으로 사용될 수 있다.
Item9 . 타입 단언보다는 타입 선언을 사용하기
- 타입 단언(as Type)보다 타입선언(: Type)을 사용해야 한다. -> 타입단언은 타입체커를 무시해버리고 강제로 타입을 지정하므로 오류가 발생할 가능성이 높아진다.
- 타입스크립트보다 타입 정보를 개발자가 더 잘 알고 있는 경우에는 타입단언문과 null 아님 *단언문(!)을 사용하면 된다.
- 화살표 함수의 반환타입을 명시하는게 조금 까다로울 때가 있는데 이하를 참조
interface Person { name: string }
const people = ["alice", "bob", "jan"].map(
(name) => ({name})
) // Person[]을 원했지만 결과는 { name:string }[]
// 그래서 타입단언을 사용하면 해결되지만, 오류가 발생할 가능성이 있음
const people = ["alice", "bob", "jan"].map(
(name) => ({name} as Person)
)
// 그러므로 이렇게
const people: Personp[] = ["alice", "bob", "jan"].map(
(name): Person => ({name})
);
Item10 . 객체 래퍼 타입 피하기
자바스크립트는 기본형과 객체 타입을 서로 자유롭게 변환할 수 있다.
위와 같이 test에 string 기본형이 타입으로써 들어가지만 charAt 메소드를 호출할 때 잠깐 String 객체 래퍼로 변환되고 charAt 함수를 사용 그리고 다시 string 기본형으로 돌아온다.
이처럼 기본형 값에 메서드를 제공하기 위해 객체 래퍼 타입이 어떻게 쓰이는지 이해해 두자
타입으로써 객체 래퍼 타입을 쓰는건 지양하고 대신에 기본형 타입을 사용해야 하는데 이는 string은 String에 할당할 수 있지만, String은 string에 할당할 수 없기 때문이다.
Item11 . 잉여 속성 체크의 한계 인지하기
잉여 속성 체크란 타입이 명시된 변수에 객체 리터럴을 할당할 때 타입스크립트는 해당 타입의 속성이 있는지, 그리고 '그 외의 속성은 없는지'를 확인하는 체크이다.
(리터럴이란 ? 리터럴(literal) 은 자바스크립트에서 객체와 배열을 정의하는 간단한 방법 ex) const test = {} )
하지만 이는 우리가 1장 구조적 타입관점에서 보면 오류가 발생하지 않아야 정상이다. 근데 왜 오류가 발생할까?
타입스크립트는 런타임 오류를 발생시키는 오류를 잡아내는 것 뿐만 아니라 의도와 다르게 작성된 코드(오타)까지 찾아주는 역할을 하는데 여기에 잉여 속성 체크가 사용되는 것이다.
여기서 우리는 잉여 속성 체크와 타입 체커가 수행하는 일반적인 타입 체크(구조적 할당 가능성 체크)의 역할이 다르다고 인식(구분)하고 있어야한다. (내가 타입스크립트를 잘 이해하지 못했던 부분이 여기인것 같다..?)
잉여 속성 체크는 객체 리터럴을 변수에 할당하거나 함수에 매개변수로 전달할 때 잉여 속성 체크가 수행되며, 중간에 임시 변수를 도입해서 할당을 하면 잉여 속성 체크를 건너뛸 수 있다는 점을 기억하자.
Item12 . 함수 표현식에 타입 적용하기
// 함수 문장
function add(a: number, b: number): number {
return a + b
}
// 함수 표현식
type BinaryFn = (a:number, b:number) => number
const add: BinaryFn = (a, b) => a + b
- 함수 문장(매개변수나 반환 값에 타입을 명시하는 방법)보다는 함수 표현식 전체에 타입 구문을 적용하는 것이 좋다.
- 같은 타입 시그니처를 반복적으로 작성한 코드가 있다면 함수 타입을 분리해내서 공통 타입으로 두는것도 좋다.
- 다른 함수의 시그니처를 참조하려면 typeof fn을 사용
Item13 . 타입과 인터페이스의 차이점 알기
- type과 interface의 다른점은 type은 유니온이 될 수도 있고, 매핑된 타입 또는 조건부 타입 같은 고급 기능에도 활용할 수 있다.
- interface는 type과 다르게 '보강(augument)'가 가능하다는 것이다. 보강이란 어떤 인터페이스를 선언(사용, declaration) 하면서 새로운 속성을 병합(merging) 하는 '선언 병합'이 되고 이는 보통 타입 선언 파일에서 자주 쓰인다.
- 타입과 인터페이스 두 가지 방법으로 모두 표현 가능하면 프로젝트의 타입 베이스에 따라 작성한다.
- API의 타입을 정의할 때는 interface를 활용하는게 좀더 좋을수? 있다고 한다. API 변경에 따라 보강기법으로 쉽게 확장할 수 있어서
// interface로는 이런 유니온 타입을 만들 수 없다.
type AorB = 'A' | 'B'
// 보강 기법
interface IState {
name: string;
capital: string;
}
interface IState { // 선언 병합
population: number
}
const test: IState = { // 에러가 발생하지 않는다.
name: "heewon";
capital: "good";
population: 77777
}
Item14. 타입 연산과 제너릭 사용으로 반복 줄이기
- DRY(don't repeat yourself) 원칙을 타입에도 최대한 적용해야 합니다.
- 타입에 이름을 붙여서 반복을 피해야 한다. extends, 인터섹션 연산자(&) 를 사용해서 인터페이스 필드의 반복을 피해야 한다.
- 타입들 간의 매핑을 위해 타입스크립트가 제공한 도구들을 공부하면 좋다. ( keyof, typeof, 인덱싱, mapped type)
- 제너릭 타입은 타입을 위한 함수와 같다. 타입을 반복하는 대신 제너릭 타입을 사용하여 타입들 간에 매핑을 해보자. (제너릭을 제한 하려면 extends를 이용)
- 타입스크립트의 표준 라이브러리에 정의된 Pick, Parial, ReturnType 같은 제너릭 타입에 익숙해지자!!
Item15. 동적 데이터에 인덱스 시그니처 사용하기
// 이게 인덱스 시그니처
// 키의 이름, 키의 타입, 값의 타입 으로 이루어져 있다.
[keyName: string]: string
- 런타임 떄까지 객체의 속성을 알 수 없을 경우에만 인덱스 시그리처를 사용하자
- 안전한 접근을 위해 인덱스 시그니처의 값 타입에 undefined를 추가하는 것을 고려하자
- 가능하다면 인덱스 시그니처 보다 정확한 타입(인터페이스, Record, 맵드타입)을 사용하는게 좋다.
Item16. number 인덱스 시그니처보다는 Array, 튜플, ArrayLike를 사용하기
number 인덱스 시그니처를 이용하면 객체의 키 값으로 number 타입을 지정할 수 있는것 처럼 보이지만, 결국에 런타임에서는 string으로써 사용되어 지고 있기 때문에 number 인덱스 시그니처보다는 다른걸 쓰자
- 배열은 객체이므로 키는 숫자가 아니라 문자열이다. 인덱스 시그니처로 사용된 number 타입은 버그를 잡기 위한 순수 타입스크립트 코드이다..! (Object.keys()로 배열의 키를 출력해보면 문자열로 나온다 , Array에 대한 타입선언은 lib.es5.d.ts에 있다.)
- 인덱스 시그니처에 number를 사용하기 보다는 Array나 튜플, 또는 ArrayLike 타입을 사용하는 것이 좋다.
- key의 타입이 불확실한 경우에는 for-in은 for-of에 비해 몇배 느리다
lib.es5.d.ts
interface Array<T> {
// ...
[n: number]: T;
}
Item17. 변경 관련된 오류 방지를 위해 readonly 사용하기
- 함수가 매개변수를 수정하지 않는다면 readonly로 선언해 두는것이 좋다. readonly 매개변수는 인터페이스를 명확하게 하며, 매개변수가 변경 되는 것을 방지할 수 있다.
- readonly를 사용하면 변경하면서 발생하는 오류를 방지할 수 있고, 변경이 일어나는 코드를 즉시 발견할 수 있다.
- const와 readonly 차이를 이해하자 const는 mutable한 친구들의 변경 기능을 이용할 수 있고 readonly는 그런 변경기능(push, pop)등을 못쓰게끔 막는다.
- readonly는 얕게 동작한다는 것을 명심하자. 어떤 객체의 readonly를 붙여다고 그 객체안의 요소 객체.요소는 readonly가 아니라는 말이다. (밑의 내용 참조)
Item18. 매핑된 타입을 사용하여 값을 동기화하기
무슨말일까? 매핑된 타입을 사용하여 값을 동기화 한다는건..?
우선 이런 상황이 있다 가정하자
props로 받는 속성중에 어떤 속성이 바뀌면 해당 컴포넌트를 업데이트 해야 하는 상황이 있다.
interface SampleProps {
a: number
b: number
c: (x; number,y: number) => number
}
a, b가 바뀌면 자식 컴포넌트를 업데이트하고 c는 업데이트 안한다고 하고 로직을 짜보면
function shouldUpdate(oldProps, newProps) {
let k: keyof SampleProps
for(k in oldProps) {
if(oldProps[k] !== newProps[k]) {
if (k !== 'c') return true
}
}
return false
}
이렇게 되는 하지만 이럴경우 SampleProps가 추가되거나 업데이트를 해야하는 조건이 바뀔때 버그가 발생하기 쉽다.
그래서 매핑된 타입을 사용하여 업데이트 대상 값을 동기화 해줘야 한다.
const REQUIRES_UPDATE: {[k in keyof SampleProps]: boolean} = {
a: true,
b: true,
c: false
}
function shouldUpdate(oldProps, newProps) {
let k: keyof SampleProps
for(k in oldProps) {
if(oldProps[k] !== newProps[k] && REQUIRES_UPDATE[k]) {
return true
}
}
return false
}
이렇게 해주면 SampleProps에 새로운 프롭이 추가되고 k in keyof SampleProps 인해 값이 추가되었다고 타입스크립트에서 알려줄 것이고 그 추가된 값을 업데이트 트리거로 삼을 것인가 말것인가를 REQUIRES_UPDATE를 변경하면 된다.
(keyof 키워드는 타입 값에 존재하는 모든 프로퍼티의 키값을 union 형태로 리턴한다.)