[TypeScript] 타입스크립트의 타입 시스템

김서현·2023년 3월 17일
0

TypeScript 스터디

목록 보기
2/4
post-thumbnail

📌 편집기를 사용하여 타입 시스템 탐색하기

타입스크립트의 타입 추론

  • 내가 의도한 타입과 타입스크립트가 추론한 타입이 다를 수 있으니 특정 시점에 타입스크립트가 값의 타입을 어떻게 이해하고 있는지 살펴보며 필요시 타입을 명시하자.

편집기상의 타입 오류 살펴보기

function getElement(elOrId: string | HTMLElement | null): HTMLElement {
  if (typeof elOrId === "object") {
    return elOrId; // 'HTMLElement | null' 형식은 'HTMLElement' 형식에 할당할 수 없습니다.
  }
}
  • 에러 메시지 해석
    자바스크립트에서 typeof null : "object"이므로 elOrId는 여전히 null일 수 있는 것! HTMLElement를 반환해야하는 함수이기 때문에 에러가 남.
    --> typeof null이 object임을 몰랐을 때를 가정한다면, 편집기상의 오류를 잘 참고하고, 살펴봐야 하는 이유!

언어에서 제공하는 라이브러리의 타입 선언 정의 적극 활용하기

declare function fetch(
input: RequestInfo, init?: RequestInit
) : Promise<Response>;
type RequestInfo = Request | String;
  • 타입스크립트가 동작을 어떻게 모델링하는지 알기 위해 편집기가 제공하는 Go to Definition(정의로 이동) 옵션을 이용하여 타입 선언 파일을 찾아보는 것을 연습하자. --> 어떻게 오류를 찾아낼지 살펴볼 수 있는 훌륭한 수단!

📌 타입이 값들의 집합이라고 생각하기

  • 할당 가능한 = ~의 원소, ~의 부분 집합
  • 타입체커 : 할당 가능한지 (하나의 집합이 다른 집합의 부분 집합인지) 검사하는 것

▶ never 타입

  • 공집합으로, 아무런 값도 할당할 수 없는 타입
  • 에러 위치처럼 보통 도달하면 안되는 곳에 할당
const x: never = 12; //error : '12' 형식은 'never'에 할당할 수 없습니다.

▶ 유닛 타입(리터럴 타입)

  • 공집합 다음으로 작은 집합으로, 한 가지 값만 포함하는 타입
type A = 'A';
type B = 'B';
type Twelve = 12;

▶ 유니온 타입

type AB = "A" | "B";
type AB12 = "A" | "B" | 12;
type AB12_2 = AB | 12;

const a: AB = "A";
const c: AB = "C"; //error
  • 실제로 흔히 다루는 타입은 대부분 버위가 무한대이므로 원소들을 일일이 추가해서 만들었다고 생각할 수 있다.
type Int = 1|2|3|4|5 // | ...

▶ Interface

interface Identified {
  id : string;
}
  • 어떤 객체가 string으로 할당 가능한 id 속성을 가지고 있다면 그 객체는 Identified
    --> ❗ 구조적 타이핑에 의해 다른 속성도 가질 수 있음을 의미.

▶ 인터섹션(&) 연산자

  • 두 타입의 교집합 계산
interface Person {
  name : string;
}
interface Lifespan {
  birth: Date;
  death?: Date;
}
type PersonSpan = Person & Lifespan;

👉 Person과 Lifespan을 둘 다 가지는 값이 인터섹션 타입에 속함.
👉 두 속성을 모두 충족해야함.

const ps: PersonSpan = {
  name: 'Alan Turing',
  birth: new Date('1912/06/23'),
  death: new Date('1954/06/07'),
};

결론 : Person과 Lifespan의 속성을 모두 가져야 한다.

▶ 유니온(|) 연산자

  • 두 타입의 합집합 계산
interface Person {
  name: string;
}
interface Lifespan {
  birth: Date;
  death?: Date;
}
type K = Person | Lifespan; 
}

👉 Person 또는 Lifespan을 충족해야 함.
👉 두 가지 모두 충족도 가능.

const sh : K = {
  name: "seohyun",
}
const sh : K = {
  birth: new Date('2001/12/19')
}
const sh : K = {
  name: "seohyun",
  birth: new Date('2001/12/19')
}

결론 : Person의 속성 또는 Lifespan의 속성, 또는 두 interface에 있는 속성 모두를 가져야 한다.

▶ keyof

  • keyof : 모든 프로퍼티의 키값(속성)을 union 형태로 반환
interface Person {
  name: string;
}
type Keyof_Person = keyof Person; //name

type K = keyof (Person | Lifespan) // 겹치는 속성이 없으므로 'never'

❗ 해석
keyof (A&B) = (keyof A) | (keyof B)
--> A&B 모두의 keyof이므로, A의 keyof와 B의 keyof의 합집합으로 해석되는 개념.
keyof (A|B) = (keyof A) & (keyof B)
--> A 또는 B의 keyof이므로, A의 keyof와 B의 keyof의 교집합으로 해석되는 개념.

결론 : keyof에서 & -> 합집합, | -> 교집합으로 반대 개념이라고 생각하면 된다.

▶ extends

  • '~의 부분집합'이라는 의미
interface Vector1D { x: number; }
interface Vector2D extends Vector1D { y: number; } // { x: number; y : number; }
interface Vector3D extends Vector2D { z: number; } // { x: number; y : number; z: number;}
  • 제너릭 타입에서 한정자로 쓰임
function getKey<K extends string>(val: any, key: K) {
  // ...
}

👉 K는 string을 상속한다 = string의 부분 집합 범위를 가지는 어떠한 타입이 된다
👉 string 리터럴 타입, string 리터럴 타입의 유니온, string 자신을 포함

// 파라미터 key가 파라미터 vals의 keyof를 타입으로 하는지 타입체크함.
function sortBy<K extends keyof T, T>(vals: T[], key: K): T[] {
  vals.sort((a, b) => (a[key] === b[key] ? 0 : a[key] < b[key] ? -1 : +1));
  return vals;
}

const pts: Point[] = [
  { x: 1, y: 1 },
  { x: 2, y: 0 },
];

// key(두번째 인자)는 "x" | "y" 이어야 함.
sortBy(pts, "x");
sortBy(pts, "y");
sortBy(pts, Math.random() < 0.5 ? "x" : "y");
sortBy(pts, "z"); //error

▶ 배열과 튜플

const list = [1, 2]; // 추론된 타입 : 무한개 있을 수 있는 number[]
const tuple: [number, number] = list; // error: list는 두개보다 더 적거나 많을 수 있기에 오류
  • [number, number]number[]의 부분집합이다.
    👉number[][number, number]에 할당될 수 없다.
const triple = [1, 2, 3];
const double: [number, number] = triple; //error : 'length' 속성의 형식이 호환되지 않습니다.

❓ 갑자기 왜 length 속성이 나왔을까?
👉 타입스크립트가 double에서 숫자의 쌍을 모델링할 때, { 0: number, 1: number, length: 2}로 모델링하기 때문에 length값이 달라 오류!

▶ Exclude

  • Exclude를 사용해 일부 타입을 제외할 시 그 결과가 적절한 타입스크립트 타입일 때만 유효
  • Exclude 정의
type Exclude<T, U> = T extends U ? never : T
type T = Exclude<string | Date, string | number>; //왼쪽 타입에서 오른쪽 타입에 할당 가능한 타입을 배제한다. --> 타입은 Date
type NonZeroNums = Exclude<number, 0>; // 결과가 적절한 타입스크립트 타입일 때만 유효하기 때문에 --> 타입은 여전히 number

📌 타입 공간과 값 공간의 심벌 구분하기

  • 같은 이름이지만 다른 것을 나타내는 경우
  • 서로 아무런 관련이 없음!
interface Cylinder { //타입
  radius: number;
  height: number;
}
const Cylinder = (radius: number, height: number) => ({ radius, height }); // 값
  • Cylinder는 타입으로도, 값으로도 쓰일 수 있어 오류를 야기하기도 함.
function calculateVolumee(shape: unknown) {
  if (shape instanceof Cylinder) { //값
    shape.radius; //error : 근데..값이 없는데?
  }
}

👉 Cylinder 타입인지 체크하려고 했지만, instanceof 는 자바스크립트의 런타임 연산자로 에 대해 연산을 함. 따라서 값으로 쓰임.

interface, type vs let, const

interface Personn {
  first: string;
  last: string;
}

const p: Person = { first: "Jane", last: "Jacobs" };

function email(p: Person, subject: string, body: string): Response {
  //...
  return new Response();
}
  • interface나 type에서는 타입으로 읽는다.
  • let이나 const에서는 값으로 읽는다.
// 타입으로 읽음
type T1 = typeof p; // "Person"
type T2 = typeof email; //Type is (p:Person, subject:string, bodyLstring) => Response

// 값으로 읽음
const v1 = typeof p; // "object"
const v2 = typeof email; // "function"

클래스

  • 클래스는 자바스크립트에서 실제 함수로 구현된다. 때문에 typeof => "function"
class Cylinder {
  radius = 1;
  height = 1;
}

// 값으로 읽음 -> JS에서 함수로 구현되므로
const v = typeof Cylinder; // "function"
  • 아래 코드에서 Cylinder는 인스턴스의 타입이 아니다. 그러므로 T의 타입은 "typeof Cylinder"로 나온다.
    👉 아직 정확히 typeof Cylinder가 무엇을 의미하지는 책에서 설명되지 않았다. 중요한건 인스턴스 타입이 아니라는 것!
// 타입으로 읽음
type T = typeof Cylinder; // T 타입 : "typeof Cylinder"
  • 그렇다면 인스턴스 타입으로 받고 싶을 때는?

    instance 인스턴스
    instance(객체)는 class를 통해서 생성한 실체 입니다. 프로그램에서는 instance를 가지고 모두 조작을 하며, class를 가지고 조작을 하지 않습니다. 왜냐 하면 class는 개념 적인 것이고 instance가 실제이기 때문 입니다.

type T = Cylinder; // T 타입 : Cylinder
const a : T = new Cylinder();

또는 InstanceType 제너릭을 사용해서생성자 함수(constructor function)를 타입을 인자로 받아, 해당 생성자 함수가 반환하는 인스턴스의 타입을 반환한다.

type C = InstanceType<typeof Cylinder>; // C 타입 : Cylinder

속성 접근자

  • 타입의 속성을 얻을 때는 반드시 obj['field']를 사용해야 한다.
  • 값으로는 가능 !
interface Person {
  first: string;
  last: string;
}

const first: Person["first"] = p["first"]; // 타입 : string
const firstt: Person["first"] = p.first; // 타입 : string
const firsttt: Person.first = p["first"]; // error

type PersonEl = Person["first" | "last"]; // 타입 : string
type Tuple = [string, number, Date];
type TupleEl = Tuple[number]; // 여기서 number가 0,1,2가 될 수 있으니까 --> 타입 : string| number | Date

interface Person {
    first: string
    last: string
}

function email(options: {person: Person; subject: string; body: string}) {
    // ...
}
  • 타입스크립트에서 구조 분해 할당을 하면, 아래와 같이 이상한 오류가 발생
    👉 값의 관점에서 Person과 string이 해석되었기 때문에 발생
    👉 person이라는 키에 접근해서 Person이라는 변수로 받아온다고 해석함.
// error
function email ({person: Person; subject: string; body: string}) {
    // ...
}

/** 해결 : 타입과 값 구분하기 */
function email ({person, subject, body} : {person: Person; subject: string; body: string}) {
    // ...
}

✅ 결론 : 타입스크립트 코드를 읽을 때 타입인지 값인지 구분하는 방법을 터득해야 한다.

0개의 댓글