[이펙티브 타입스크립트] 2장 전반부 타입스크립트의 타입 시스템

JIY00N·2023년 7월 30일
4

TypeScript

목록 보기
2/9
post-thumbnail
post-custom-banner

2023.07.30~2023.08.03 아이템6~11

2장에서 타입스크립트의 타입 시스템을 이해해봅시다.

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


  • 타입스크립트 설치 시, 두 가지를 실행할 수 있다.

1. 타입스크립트 컴파일러

2. 단독으로 실행할 수 있는 타입스크립트 서버
-> 편집기(VSC)를 통해 언어 서비스를 제공하도록 설정
언어 서비스: 코드 자동 완성, 오류 확인, 리팩토링, 타입 추론, 정의 찾기 등

  • 1번이 주된 목적이지만, 2번도 1번 못지않게 중요하다.

  • 정의 찾기(Go to Definition)를 통해 타입 선언 파일(d.ts)을 찾아보면,
    타입스크립트가 동작을 어떻게 모델링 하는지 알 수 있다.

❓ 번외 pg36

  • 자바스크립트에서 typeof nullobject이다.
    typeof undefinedundefined

🎯 요약

타입스크립트에서 언어 서비스를 잘 이용하자


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


  • 타입을 값의 집합으로 생각하면 이해하기 편하다.
  • A는 B를 상속 = A는 B에 할당 가능 = A는 B의 서브 타입 = A는 B의 부분 집합
   타입스크립트 용어       집합 용어    
never공집합 (값 할당 불가)
unknown전체 집합 (어떠한 값이든 할당 가능)
리터럴 타입, 유닛 타입원소가 1개인 집합
유니온 타입원소가 두개 이상인 집합
값이 A에 할당 가능값 ∈ A (값이 A의 원소)
A가 B에 상속A ⊆ B (A가 B의 부분집합)
A | B (A와 B의 유니온)A ⋃ B (A와 B의 합집합)
A & B (A와 B의 인터섹션)A ⋂ B (A와 B의 교집합)
A와 B의 속성을 모두 가짐

❓pg41 규칙이 속성에 대한 인터섹션에 관해서는 맞지만, 두 인터페이스의 유니온에서는 그렇지 않다.
-> 유니온이여도 공통된 속성이 있으면 통과되지만, 없으면 정해질 수 없어서 never

interface Person{
  name: string;
}
interface Lifespan{
  birth: Date;
  death?: Date;
}
type k = keyof(Person | Lifespan); // 타입이 never
keyof (A&B) = (keyof A) | (keyof B)
keyof (A|B) = (keyof A) & (keyof B)

❓pg46

  • 한 객체의 추가적인 속성이 타입 선언에 언급되지 않더라도 그 타입에 속할 수 있다.
    (= 구조적 타이핑)

🎯 요약

타입을 값의 집합으로 생각하자


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


  • 타입스크립트에서는 타입의 공간(타입)값의 공간(값)분리되어있다.

  • 타입스크립트의 심벌(식별자)타입으로 쓸 수도 있고, 으로 쓸수도 있다.

type Fruits = "apple" | "cherry"
const Fruits = "banana"

-> 타입 정보는 컴파일 과정에서 사라지기 때문에, 심벌이 사라지면 그건 타입이다.
안 사라지면 값. TS playground 에서 감 익히기
✅ 컴파일 과정에서 사라지는 것들: :, as, 타입 주석, type, interface

  • 타입에서와 값에서 쓰일 때, 다른 기능을 하는 연산자 typeof
interface Person {
  name: string;
  age: number;
  fruits: 'apple' | 'mango' | 'peach';
}

const person1: Person = {
  name: 'lee',
  age: 25,
  fruits: 'mango',
};

// 1. 타입에서의 typeof
// 타입스크립트 타입으로 반환
type Lee = typeof person1; // type Lee = Person

// 2. 값에서의 typeof
// 런타임에 식별자의 타입을 반환
const leeType = typeof person1;
console.log(leeType); // "object" 
  • 타입과 값, 동시에 가능한 class, enum

  • class

    클래스의 타입은 typeof 클래스(생성자 함수를 나타냄),
    인스턴스의 타입은 클래스

// 1. 값으로 사용
class Stone {}
console.log(typeof Stone); // 클래스 자체 타입은 function
console.log(typeof new Stone()); // 클래스로 만들어낸 인스턴스 타입은 object

// 2. 타입으로 사용
// 타입에서의 Stone 클래스의 타입은? typeof Stone?
type StoneClassType = typeof Stone 
// type StoneClassType = typeof Stone
// 클래스의 생성자 함수의 타입은 typeof Stone

// Stone의 인스턴스 타입은 Stone
const newStone = new Stone();
type TypeofNewStone = typeof newStone;
// type TypeofNewStone = Stone

//  타입과 값으로 쓰임
class Cylinder{
  radius = 1;
  height = 1;
}
function calculateVolume(shape: unknown){
  if(shape instanceof Cylinder){
    shape // 정상 타입은 Cylinder
    shape.radius // 정상 타입은 number
  }
}

🎯 요약

타입스크립트 코드를 읽을 때, 타입인지 값인지 잘 구분하자


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


  • 타입스크립트에서 변수에 값을 할당하고 타입을 부여하는 2가지 방법
    1. 타입 선언 - 변수: Type 변수에 타입을 붙임
    2. 타입 단언 - as Type 강제로 타입 지

타입 선언을 사용하자

interface Person { name: string };
const alice: Person = {name: 'Alice'}; // 1. 타입 선언
const bob = {name: 'Bob'] as Person; // 2. 타입 단언          
  • 타입 단언을 사용하는 경우도 있다.
    -> DOM에서 Element를 찾는 API를 사용할 때,
    -> 개인 의견: 그치만 제너릭을 사용할수도 있다 ?
  • 타입 단언을 하려면 객체의 현재 타입이 단언될 타입의 상위 집합 혹은 부분 집합이어야 한다.
const button = document.querySelector("#myButton");
// const button: Element | null

// 개발자가 확신할 수 있다면 타입 단언으로 타입을 변형해 줄 수 있다.
const button = document.querySelector("#myButton") as HTMLButtonElement;
// const button: HTMLButtonElement

// querySelector 함수 시그니처
querySelector<K extends keyof HTMLElementTagNameMap>(selectors: K): HTMLElementTagNameMap[K] | null;
querySelector<K extends keyof SVGElementTagNameMap>(selectors: K): SVGElementTagNameMap[K] | null;
querySelector<K extends keyof MathMLElementTagNameMap>(selectors: K): MathMLElementTagNameMap[K] | null;
/** @deprecated */
querySelector<K extends keyof HTMLElementDeprecatedTagNameMap>(selectors: K): HTMLElementDeprecatedTagNameMap[K] | null;
querySelector<E extends Element = Element>(selectors: string): E | null;
/* E는 Element를 상속받고, 
E의 타입이 명시되어 있지 않으면 기본값으로 Element를 사용
파라미터는 string, 리턴 타입은 E or null
=> 제너릭에 리턴받고 싶은 타입을 명시해줄 수 있음
*/

// 제너릭을 통한 타입 변경
const button = document.querySelector<HTMLButtonElement>('#myButton'); 
// const button: HTMLButtonElement | null

🎯 요약

불가피한 상황 제외하고, 타입 단언(as Type)보다 타입 선언(: Type)을 사용하자


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


  • 객체 래퍼 타입: 대문자로 시작, Number, String, Boolean

  • 원시 타입: 소문자로 시작, number, string, boolean

  • 타입스크립트에서 변수 선언시 객체 래퍼 타입을 지양해야하는 이유
    -> 타입스크립트는 자바스크립트의 동작을 모델링 한다.
    -> 자바스크립트에서 선언되는 변수들은 모두 원시 타입으로 선언된다.
    -> 타입스크립트에서 변수를 객체 래퍼 타입으로 선언을 하게 된다면, 자바스크립트에서 변수의 동작과 다르게 프로그래밍 될 수 있다.

function isGreeting(phrase: String){
  return ['hello', 'hi'].includes(phrase); 
  // 오류 -> String타입은 string타입에 할당 불가능
}

🎯 요약

타입스크립트 객체 래퍼 타입을 지양하고, 기본형 타입을 쓰자


아이템 11 잉여 속성 체크의 한계 인지하기


  • 타입 검사에 영향을 미치는 구조적 타이핑과 잉여 속성 체크가 있다.

  • 잉여 속성 체크

    1. 타입에 선언된 속성이 있는지, 그 외의 속성은 없는지 확인
    2. 객체 리터럴을 변수에 할당하거나 함수에 매개변수로 전달할 때, 잉여 속성 체크가 수행됨
    3. 객체 리터럴에서만 잉여 속성 체크가 동작함 -> 엄격한 객체 리터럴 체크라고도 불림
    4. 오타 방지
  • 구조적 타이핑 검사잉여 속성 체크별도의 과정

  • { name: "Alice", age: 25, occupation: "Engineer" }라는 객체 리터럴을 미리 변수에 할당(구조적 타이핑)객체 리터럴을 직접 할당하는 상황(잉여 속성 체크)과는 해당 변수를 할당하는 상황과 동작은 같지만, 타입스크립트는 다르게 해석하고 에러를 후자(잉여 속성 체크)의 상황만 에러를 발생시킨다.

1. 구조적 타이핑 관점

// 구조적 타이핑
interface Person {
  name: string;
  age: number;
}

const person1 = { name: "John", age: 30 };
const kim: Person = person1; // 정상 동작

const person2 = { name: "Alice", age: 25, occupation: "Engineer" };
const lee: Person = person2; // 정상 동작

2. 잉여 속성 체크 관점

// 잉여 속성 체크
interface Person {
  name: string;
  age: number;
}

const person1: Person = { name: "John", age: 30 }; // 정상 동작
const person2: Person = { name: "Alice", age: 25, occupation: "Engineer" };
// 여기서 문제 발생, 잉여 속성 체크

/*
잉여 타입 체크 에러메시지
Type '{ name: string; age: number; occupation: string; }' is not assignable to type 'Person'.
Object literal may only specify known properties, and 'occupation' does not exist in type 'Person'.ts(2322)
*/
  • 잉여 속성 체크의 한계점
  1. 타입 단언문을 사용할 때, 적용 안됨
  2. 인덱스 시그니처 사용시, 잉여 속성 체크 무력화
// 1. 타입 단언
interface Person {
  name: string;
  age: number;
}

const me = {
  name: "yoon",
  age: 25,
  gender: "F"
} as Person

// 2. 인덱스 시그니처
type Coffee = {
  flavor: string;
  [options: string]: unknown;
};
const latte: Coffee = {
  flavor: 'good',
  milk: true,
  price: 5000,
};

선택적 속성만(모든 속성이 Optional)가지는 약한 타입에서는 '공통 속성 체크'가 동작함
공통 속성 체크: 약한 타입은 임시 변수로 할당해도 잉여 속성 체크와 비슷하게 검사를 진행한다.

type WeakType = {
  some?: string;
  thing?: number;
  prop?: boolean;
};

const opts = { tHing: 12 };
const o: WeakType = opts;
// Type '{ tHing: number; }' has no properties in common with type 'WeakType

🎯 요약

잉여 속성 체크와 구조적 타이핑의 차이점을 구분하고, 잉여 속성 체크의 한계점을 기억하자


☑️ 참고 자료
아이템7 잉여 속성 체크와 구조적 타이핑
아이템9 querySelector

profile
블로그 이전 했습니다. https://yoon-log.vercel.app/
post-custom-banner

0개의 댓글