[독서] 이펙티브 타입스크립트 아이템 6 - 10

dante Yoon·2021년 8월 29일
0

독서

목록 보기
5/38
post-thumbnail

독서 후 개인적으로 정리한 내용을 기록한 글입니다.

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

타입스크립트를 설치하면 아래 두 가지가 실행할 수 있게 된다.
1. 타입스크립트 컴파일러인 tsc
2. 단독으로 실행할 수 있는 타입스크립트 서버인 tsserver

편집기에서 타입스크립트 언어 서비스를 적극 활용한다.
편집기를 사용하면 어떻게 타입 시스템이 동작하는지, 타입을 추론하는지 개념으 잡을 수 있다.

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

런타임에 모든 변수는 타입이 사라지고 자바스크립트에서 제공하는 각자의 고유한 값을 가진다.
그러나 코드가 실행되기전 타입체커가 오류를 체크하는 순간에는 타입을 가지고 있다. 이때의 타입은

할당 가능한 값들의 집합

이라고 바꾸어 말할 수 있다.
그리고 이 집합은 다시 바꾸어

타입의 범위

라고 부르기도 한다.

모든 숫자값의 집합number 타입이라고 생각할 수 있다.
null과 undefined strictNullChecks 설정 여부에 따라 number에 해당이 될 수도, 안될수도 있다.

never는 공집합에 해당하며 가장 좁은 타입으로 어떤 값도 해당 타입에 할당할 수 없다.

공집합 다음으로 작은 집합은 한 가지 값만 포함하는 리터럴(literal) 타입이다.

type A = "A";
type C = "C"; 
type Thirteen = 13;

두, 세개의 집합을 가지기 위해서는 값 집합의 합집합을 의미하는 유니온 타입을 사용한다.

type AC = "A" | "C";

타입체커의 역할은 하나의 집합이 다른 집합의 부분집합인지를 검사하는 것이다.
다음의 코드에서 B는 type AC의 부분집합이 아니므로 할당 오류가 발생한다.

const b: AC = "B"; 
// Type '"B"' is not assignable to type '"A" | "C"'.ts(2322)

실제 프로그램을 만들면서 작성하는 타입은 앞서 말한 예제들보다는 훨씬 복잡하다.

interface Identified {
  id: string;
}

Identified 가 타입 범위 내의 값들에 대한 설명이라고 할 때
어떤 객체가 string 타입의 id 속성을 가지고 있다면 그 객체는 Identified이다.

구조적 타이핑 규칙들은 특정 값의 타입이 정해져있다고 하더라도 다른 속성 값들 또한 가질 수 있음을 의미한다.

어려워지기 시작했다.

값의 집합을 타입이라고 생각할때 아래 & 타입 연산자는 교집합을 의미하기에 PersonSpan은 공집합으로 받아들이는게 자연스럽다고 생각할 수 있다.

interface Person {
  name: string
}
interface Lifespan {
  birth: Date;
  death?: Date;
}
type PersonSpan = Person & Lifespan;

타입 연산자는 인터페이스의 속성이 아닌 값의 집합(타입의 범위)에 적용되며,
추가적인 속성을 가지는 값도 그 타입에 속하기 때문에
Person과 Lifespan의 속성을 모두 가지는 값이 인터섹션 타입에 속하게 된다.

 const ps: PersonSpan ={
  name: "Dante",
  birth: "1900/12/12",
  death: "3000/12/12",
 }

유니온 타입에 대해서는 다르게 동작한다.

type K = keyof (Person | Lifespan); // 타입이 never

PersonSpan 타입을 선언하는 일반적인 방법은 extends 키워드를 사용하는 것이다.

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

extends는 집합에서 ~의 부분집합 이라는 의미로 받아들일 수 있다.
~의 부분집합서브타입이라는 용어로 사용할 수 있으며 PersonSpan은 Person의 부분집합이다.

기존에 타입스크립트를 사용하던 사람은 아이템 7에 대해 혼란함을 느낄 수 있다.
위에 말대로라면, Person 타입에 PersonSpan 타입을 대입해도 오류가 나면 안되는 것이기 때문이다. 근데 실상은 다른 경우를 경험해보았다.

interface Person {
  name: string
}

interface APerson extends Person {
  age: number;
  number: number;
}

const a: Person = {
  name: "",
  number: "" // 타입 에러
}
/**
Type '{ name: string; number: string; }' is not assignable to type 'Person'.
  Object literal may only specify known properties, and 'number' does not exist in type 'Person'.ts(2322)
 **/

음, 그런데 책이 잘못 설명하고 있는 것이 아니다. 이건 우리가 보통 타입에 값을 선언할 때 Object literal를 사용했기 때문이다. 다음 코드는 정상 동작한다.

const b =  {
  name: "",
  number: ""
}
const c : Person = b;

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

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

interface Param {
  radius: number;
  height: numbe;r
}

const Param = (radis: number, height: number) => ({radius, height});

interface Param에서 Param은 타입으로 쓰인다.
param 함수의 시그니처 매개변수 타입은 인터페이스 Param과 같으나 서로 아무 연관이 없다.

instanceof는 런타임 연산자이기 때문에 instanceof Param은 interface Param이 아닌 함수 Param 타입을 의미한다. 인터페이스와 값의 이름이 같으면 이와 같은 혼란을 초래할 수 있다.

class와 enum은 상황에 따라 타입과 값 두가지 모두 사용 가능한 예약어 이다.

연산자 중에서도 typeof는 타입에서 쓰일 때와 값에서 쓰일 때 다른 기능을 한다.

타입의 관점에서 typeof는 값을 읽어서 타입스크립트 타입을 반환한다.

const p = {
  ...
}
  
type T = typeof p

값의 관점에서 typeof는 자바스크립트 런타임의 typeof 연산자가 된다.

if(typeof "string" === "string){
  ...
}

값 공간의 typeof는 대상 심벌의 런타임 타입을 가리키는 문자열을 반환한다.

아이템 9 타입 단언보다는 타입 선언을 사용하기

다음 두가지 방법은 동일한 결과로 보인다.

interface Some { some: string};
const someWhere: Some = {some: "where"};
const someHow ={some: "how"} as Some;

someWhere는 변수에 타입선언을 붙여서 그 값이 선언한 타입임을 명시하는 반면
두번 째 문장은 타입 단언을 수행한다. 타입스크립트의 추론과는 무관하게 어떤 변수든 Some으로 단언한다.

정적 타입을 사용하는 이유는 런타임의 오류를 방지하기 위해서인데 단언을 사용하는 것은 타입스크립트가 제공하는 장점 중 하나를 버리는 상황을 만들 수 있다.

타입 선언이 필요한 경우는 타입스크립트가 추론을 정확히 하지 못할 때이다.
DOM 엘리먼트에 대해서는 타입스크리트가 충분한 추론을 제공해주지 못한다.

document.querySelector("#myButton").addEventListener("click", e => {
  e.currentTarget // 타입은 EventTarget
  const button. = e.currentTarget as HTMLButtonElement
  button // 타입은 HTMLButtonElement
})

이떄 이벤트 currentTarget 속성이 가르키는 것이 button인지 아닌지 타입스크립트는 알 수 없다.

그리고 리엑트의 useRef를 사용해 돔을 직접 제어할 때도 다음과 같이 단언문을 사용할 수 있다.
이때도 단언문보다는 제너릭을 이용해 추론하는게 낫다.

import { useRef } from "react";
const SomeComponent: React.FC  = () => {
  const buttonRef = useRef<HTMLButtonElement>(null); 
  // 혹은 
  const buttonRef = useRef() as HTMLButtonElement 
}

아이템 10 객체 래퍼 타입 피하기

자바스크립트에서는 일곱가지 기본형 타입이 존재한다.
기본형들은 메서드를 가지지 않는데 toUpperCase와 같은 메서들를 사용할 수 있는 것은 메소드 호출시 자동으로 객체로 래핑되기 때문이다.

따라서 기본형과 String 과 같은 객체 래퍼가 동일하게 동작한다고 생각할 수 있지만, 항상 그런 것은 아니다. 래퍼는 객체 타입이기에 오직 자기 자신하고만 동일하다.

"hello" === new String("hello") //false
new String("hello") === new String("hello") // false

string과 String은 타입스크립트에서 별도의 타입이기 때문에 오타를 작성해서 함수 시그니처를 잘못된 타입으로 매핑하지 말자.

대부분의 경우 래퍼 타입을 직접 사용할 일은 없다.

profile
성장을 향한 작은 몸부림의 흔적들

0개의 댓글