이펙티브 타입스크립트 1장 ( 1 ~ 5 )
포스트
취소

이펙티브 타입스크립트 1장 ( 1 ~ 5 )

해당 포스트는 이펙티브 타입스크립트 1장을 읽고 정리한 포스트입니다.
책의 모든 내용을 작성하는 것이 아닌 주관적인 기준에 따라 필요한 정보만 정리했습니다.

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

📌 type/interface에 instanceof 사용하기

typeinterfaceinstranceof를 사용하면 오류가 발생합니다.
typescript의 컴파일 과정에서 typeinterface가 제거되기 때문입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface Square {
  width: number;
}
interface Rectangle extends Square {
  height: number;
}
type Shape = Square | Rectangle;

const getArea = (shape: Shape) => {
  // // "Square", "Rectangle", "Shape"같은 타입은 컴파일과정에서 제거됨
  // if(shape instanceof Square) {}
  // 방법1) class로 만들기 ( "Square", "Rectangle" )

  // 방법2)
  if ("height" in shape) return shape.width * shape.height;
  return shape.width ** 2;

  // 방법3) 태그 기법... 생략
};

📌 런타임 타입은 다를 수 있다.

아래의 코드를 보면 TypeScript에서는 전혀 문제가 없습니다.
default:에 들어갈 가능성이 전혀 없는 코드라고 생각할 수 있죠.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
interface ApiUserResponse {
  // ... 나머지 생략
  gender: boolean;
}

(async () => {
  const user: ApiUserResponse = await (await fetch("대충 유저 정보를 요청하는 URL")).json();

  switch (user.gender) {
    case true:
      // ...
      break;
    case false:
      // ...
      break;
  
    default:
      console.log("여기에 들어올 수 있는 경우의 수가 있을까?")
      break;
  }
})()

하지만 런타임에서는 TypeScript의 판단과 다른 동작이 발생할 수 있습니다.
만약 ApiUserResponsefetch의 응답 값이 상이하다면 즉, gender라는 프로퍼티가 boolean이 아니고 string이라면 default가 실행되게 됩니다.
TypeScript에서는 발생할 수 없는 상황이라고 판단하지만 런타임에서는 다를 수 있음을 인지해야합니다.

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

interface, type 등은 TypeScript를 컴파일하는 과정에서 모두 제거되기 때문에 JavaScript가 실행하는 런타임의 성능에는 전혀 영향을 주지 않습니다.

📌 Item 4 ( 구조적 타이핑에 익숙해지기 )

덕 타이핑과 구조적 타이핑에 대한 명확한 차이가 이해가지 않아서 두 개의 단어를 혼용해서 설명하겠습니다.
덕 타이핑(구조적 서브타이핑)과 구조적 타이핑에 대한 개념 정리와 예시를 살펴보겠습니다.

0️⃣ 덕 타이핑과 구조적 타이핑

  • 덕 타이핑: 상속 관계를 명확하게 명시하지 않은 경우에도 필요한 프로퍼티를 가지고 있다면 타입 호환을 허용하는 것
    ( 즉, 상속 관계에 관계 없이 타입 호환을 허용한다는 의미 )
  • 구조적 타이핑: 오직 멤버만으로 타입을 관계시키는 방식

덕 테스트란 “만약 어떤 새가 오리처럼 걷고, 헤엄치고, 꽥꽥거리는 소리를 낸다면 나는 그 새를 오리라고 부를 것이다.”

덕 타이핑이란 덕 테스트에서 유래된 단어라고 합니다.
그러면 덕 테스트의 의미에 대해서 이해한대로 설명해보겠습니다.

"만약 어떤 새가 오리처럼 걷고, 헤엄치고, 꽥꽥거리는 소리를 낸다면"이 말의 의미는 “나는 이 새가 오리인지 비둘기인지에는 관심이 없다.” “그냥 우리가 아는 오리처럼 행동하는지에 대해서만 관심이 있다.”로 이해했고, "나는 그 새를 오리라고 부를 것이다."이 말의 의미는 대상이 뭐든 오리라고 부를거라는 의미로 이해했습니다.

즉, “실제로 오리인지 아닌지 그거는 관심이 없고, 오리처럼 행동한다면 나는 그냥 오리라고 판단한다.”입니다.

이 개념을 코드로 옮겨본다면 아래 예시에서 “내가 받은 객체가 Person이든 Duck이든 상관 없고, nameage라는 프로퍼티(멤버)만 가지면 오류 없이 실행한다.”라고 이해했습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Person {
  name: string;
  age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

class Duck {
  name: string;
  age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

const whoAreYou = (person: Person) => console.log(`${person.name} : ${person.age}`);

const person = new Person("john", 26);
const duck = new Duck("dd", 2);

whoAreYou(person);

// 이상하게 느껴질 수 있습니다. "Person"을 인자로 받는데 "Duck"이 들어가도 문제없이 동작하죠.
// 이게 TS가 구조적 타이핑을 따르기 때문에 문제없이 동작합니다.
// 즉, "Person"의 멤버인 "name"과 "age"만 갖고 있다면 다른 클래스의 객체라도 상관이 없다는 의미입니다.
whoAreYou(duck);

// 만약 TS가 명목적 서브타이핑을 따른다면 위 코드에서 오류가 발생할 것입니다.
// C++, Java 같은 언어는 위와 같은 형태라면 오류가 발생하죠.

1️⃣ 구조적 타이핑 더 알아보기

구조적 타이핑에 대해 모르고 있다면 아래와 같은 코드에서 원인을 정확하게 파악하기 힘듭니다.
( + TS의 타입은 항상 확장될 수 있음. (2) )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Person {
  name: string;
  age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

class Duck {
  name: string;
  age: number;
  // 추가
  kind: string;
  constructor(name: string, age: number, kind: string) {
    this.name = name;
    this.age = age;
    this.kind = kind;
  }
}

const whoAreYou = (person: Person) => {
  for (const key in person) {
    console.log(person[key]); // "key"가 "string"타입이라서 "Person'의 인덱스로 사용할 수 없다는 에러가 발생합니다.
  }
  // 위에서 에러가 발생하는 이유는 (2)같은 경우 때문입니다.
  // (2)를 허용하기 때문에 "key"가 반드시 "Person"이 가지고 있다고 확신할 수 없죠.
};

const person = new Person("john", 26);
const duck = new Duck("dd", 2, "bird");

whoAreYou(person);
// "Duck"에 "kind"라는 타입이 추가되었는데도 문제 없이 동작합니다. (2)
// TS에서는 타입이 항상 확장될 수 있기 때문에 필수 멤버만 채우면 추가로 들어가 있는 것은 오류를 내지 않습니다.
whoAreYou(duck);

// 하지만 아래와 같은 코드는 오류를 발생합니다.
whoAreYou({
  name: "bin",
  age: 12,
  kind: "person", // 개체 리터럴은 알려진 속성만 지정할 수 있으며 'Person' 형식에 'kind'이(가) 없습니다.
});

📮 레퍼런스

  1. « 이펙티브 타입스크립트 » ( 댄 밴더캄 지음, 장원호 옮김, 인사이트, 2021 )
  2. TypeScript 문서
  3. 블로그 - 덕 타이핑과 구조적 타이핑
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

script의 defer vs async

JSDoc ( .js에서 타입 적용하기 )