Skip to content

Latest commit

 

History

History
272 lines (192 loc) · 10.9 KB

File metadata and controls

272 lines (192 loc) · 10.9 KB

Item8 - Item10

[Item8] 타입 공간과 값 공간의 심벌 구분하기

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

심벌은 이름은 같더라도 속하는 공간에 따라 다른 것을 나타낼 수 있기 때문에 혼란스러울 수 있다.

interface Cylinder {
	radius: number;
	height: number;
}

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

interface Cylinder에서 Cylinder는 타입으로 쓰인다. const Cylinder에서는 값으로 쓰인다.

function calculateVolume(shape: unknown) {
	if (shape instanceof Cylinder) {
		shape.radius // {} 형식에 'radius' 속성이 없다	
	}
}

instanceof를 이용해 shape가 Cylinder 타입인지 체크하려고 함. 그러나 instanceof는 자바스크립트의 런타임 연산자이고, 값에 대해서 연산하기 때문에 instanceof Cylinder는 타입이 아니라 함수를 참조한다.

위 내용으론 한 심벌이 타입인지 값인지는 알 수 없다. 어떤 형태로 사용되는지는 문맥을 살펴야 한다.

타입스크립트 코드에서 타입과 값은 번갈아 나올 수 있다. 타입 선언(:) 또는 단언문(as) 다음에 나오는 심벌은 타입인 반면, = 다음에 나오는 모든 것은 값이다.

interface Person {
	first: string;
	last: string;
}
const p: Person = { first: 'Jane', last: 'Jacobs' };
// Person은 타입, { first: 'Jane', last: 'Jacobs' }은 값

function email(p: Person, subject: string, body: string): Response {}
//       값    값   타입     값        타입     값     타입      타입

연산자 중에서도 타입에서 쓰일 때와 값에서 쓰일 때 다른 기능을 하는 것들이 있다.

typeof

type T1 = typeof p; // 타입은 Person
type T2 = typeof email; // 타입은 (p: Person, s: string, b: string): Response

const v1 = typeof p; // 값은 'object'
const v2 = typeof email; // 값은 'function'

타입의 관점에서 typeof는 값을 읽어서 타입스크립트 타입을 반환한다. 타입 공간의 typeof는 보다 큰 타입의 일부분으로 사용할 수 있고, type구문으로 이름을 붙이는 용도로도 사용할 수 있다.

값의 관점에서 typeof는 자바스크립트 런타임의 typeof 연산자가 된다. 값 공간의 typeof는 대상 심벌의 런타임 타입을 가리키는 문자열을 반환하며, 타입스크립트 타입과는 다르다.

class Cylinder {
	radius = 1;
	height = 1;
}
function calculateVolume(shape: unknown) {
	if (shape instanceof Cylinder) {
		shape // 정상, 타입은 Cylinder
		shape.radius // 정상, 타입은 number
	}
}

class 키워드는 값과 타입 두 가지로 모두 사용된다. 따라서 클래스에 대한 typeof는 상황에 따라 다르게 동작한다.

const v = typeof Cylinder; // 값이 'function'
type T = typeof Cylinder; // 타입이 typeof Cylinder

클래스가 자바스크립트에서 실제 함수로 구현되기 때문에 첫 번째 줄의 값은 function이 된다.

두 번째 줄의 타입은 Cylinder가 인스턴스의 타입이 아니라는 점이 중요하다. 실제로는 new 키워드를 사용할 때 볼 수 있는 생성자 함수다.

declare let fn: T;
const c = new fn(); // 타입이 Cylinder

InstanceType 제너릭을 사용해 생성자 타입과 인스턴스  타입을 전환   있다.
type C = InstanceType<typeof Cylinder>; // 타입이 Cylinder

속성 접근자인 []는 타입으로 쓰일 때에도 동일하게 동작한다. 그러나 obj['field']와 obj.field는 값이 동일하더라도 타입은 다를 수 있다. 따라서 타입의 속성을 얻을 때에는 반드시 첫 번째 방법을 사용해야 한다.

type PersonEl = Person['first' | 'last']; // 타입은 string
type Tuple = [string, number, Date];
type TupleEl = Tuple[number]; // 타입은 string | number | Date
  • 속성 접근자는 아이템14에서 자세히 다룬데요~

두 공간 사이에서 다른 의미를 가지는 코드 패턴

  • 값으로 쓰이는 this는 자바스크립트의 this 키워드이다. 타입으로 쓰이는 this는 일명 '다형성 this'라고 불리는 this의 타입스크립트 타입이다. 서브 클래스의 메서드 체인을 구현할 때 유용
  • 값에서 &와 | 는 AND와 OR 비트연산이다. 타입에서는 인터섹션과 유니온이다.
  • const는 새 변수를 선언하지만, as const는 리터럴 또는 리터럴 표현식의 추론된 타입을 바꾼다.
  • extends는 서브 클래스(class A extends B) 또는 서브타입(interface A extends B) 또는 제너널 타입의 한정자(Generic)를 정의할 수 있다.
  • in은 루프(for (key in object)) 또는 매핑된 타입에 등장한다.

타입스크립트 코드가 잘 동작하지 않는다면 타입 공간과 값 공간을 혼동해서 잘못 작성했을 가능성이 크다.

타입과 값은 비슷한 방식으로 쓰는 점이 처음에는 혼란스러울 수 있지만 요령을 터득하기만 한다면 마치 연상 기호처럼 무의식적으로 쓸 수 있다!

[Item9] 타입 단언보다는 타입 선언을 사용하기

타입스크립트에서 변수에 값을 할당하고 타입을 부여하는 방법은 두가지가 있다.

interface Person { name: string };

const alice: Person = { name: 'Alice' };
const bob = { name: 'Bob' } as Person;

첫번째 Person은 변수에 타입선언 을 붙여서 그 값이 선언된 타입임을 명시한다.

두번째 Person은 as Person 을 붙여서 타입단언 을 수행한다. 그러면 타입스크립트가 추론한 타입이 있더라도 Person타입으로 간주한다.

타입 단언보다 타입 선언을 사용하는 게 좋다!

const alice: Person = {};  // error: name이 없다.
const bob = {} as Person;  // error 없음

타입 선언은 할당되는 값이 해당 인터페이스를 만족하는지 검사한다. 속성을 추가할 때도 마친가지다.

const alice: Person = {
	name: 'Alice',
	occupation: 'TypeScript Developer'
}; // error: Person 형식에 occupation이 없다.

const bob = {
	name: 'Bob',
	occupation: 'JavaScript Developer'
} as Person; // error 없음

타입 선언문에서는 잉여 속성 체크가 동작했지만, 단언문에서는 적용되지 않음. 타입 단언이 꼭 필요한 경우가 아니라면, 안전성 체크도 되는 타입 선언을 사용하는 게 좋다.

화살표 함수의 타입 선언은 추론된 타입이 모호할 때가 있다.

const people = ['alice', 'bob', 'jan'].map(name => ({name}));
// Person[]을 원했지만 결과는 { name: string; }[]
const people = ['alice', 'bob', 'jan'].map(name => ({name} as Person));
// 문제가 해결되는 것처럼 보이지만 런타입에 문제가 발생

const people = ['alice', 'bob', 'jan'].map(name => ({} as Person));
// 오류 없음

그럼 어떻게?

const people: Person[] = ['alice', 'bob', 'jan'].map(
	(name): Person => ({name})
);

(name): Person은 name의 타입이 없고 반환 타입이 Person이라 명사한다. 그러나 (name: Person)은 name의 타입이 Person임을 명시하고 반환 타입은 없기 때문에 오류를 발생한다.

함수 체이닝이 연속되는 곳에서는 체이닝 시작에서부터 명명된 타입을 가져야 한다. 그래야 정확한 곳에 오류가 표시된다.

타입 단언이 꼭 필요한 경우도 있다.

타입 단언은 타입 체커가 추론한 타입보다 사용자가 판단하는 타입이 더 정확할 때 의미가 있다. 예를 들어, DOM 엘리먼트에 대해서는 타입스크립트보다 사용자가 더 정확히 알고 있다.

documemt.querySelector('#myButton').addEventListener('click', e => {
	e.currentTarget  // 타입은 EventTarget
	const button = e.currentTarget as HTMLButtonElement;
	// button의 타입은 HTMLButtonElement로 지정
});

타입스크립트는 DOM에 접근할 수 없기 때문에 #myButton이 몬지 모른다. 그리고 이벤트의 currentTarget이 같은 버튼이어야 하는 것도 모른다.

(!)를 사용해서 null이 아님을 단언하는 경우

const elNull = document.getElementById('foo'); // HTMLElemet | null
const el = document.getElementById('foo')!; // HTMLElement

변수의 접두사로 쓰인 !는 boolean의 부정문이다. 그러나 접미사로 쓰인 !는 그 값이 null이 아니라는 단언문으로 해석된다.

!를 일반적인 단언문처럼 생각해야 한다. 단언문은 컴파일 과정 중에 제거되므로, 타입 체커는 알지 못하지만 그 값이 null이 아니라고 확신할 수 있을 때 사용해야 한다.

그렇지 않다면, null인 경우를 체크하는 조건문을 사용해라!

[Item10] 객체 래퍼 타입 피하기

객체 외 기본형 7가지 타입

  • string
  • number
  • boolean
  • null
  • undefined
  • symbol
  • bigint

기본형들은 불변이며 메서드를 가지지 않는다는 점에서 객체와 구분된다.

그런데, 기본형인 string의 경우 메서드를 가지고 있는 것처럼 보인다. 근데 메서드 아님!

> 'primitive'.charAt(3)
"m"

string '기본형'에는 메서드가 없지만, 자바스크립트에는 메서드를 가지는 String '객체' 타입이 정의되어 있다. 자바스크립트는 기본형과 객체 타입을 서로 자유롭게 변환한다.

string 기본형에 charAt 같은 메서드를 사용할 때,

기본형을 String 객체로 래핑(wrap) -> charAt 메서드 호출 -> 래핑한 객체 버림

Example (String.prototype을 몽키-패치했을 때 내부적인 동작)

// 실제로는 이렇게 쓰지 말래요.
const originalCharAt = String.prototype.charAt;
String.prototype.charAt = function(pos) {
	console.log(this, typeof this, pos);
	return originalCharAt.call(this, pos);
};
console.log('primitive'.charAt(3));

## result
[String: 'primitive'] 'object' 3
m

메서드 내의 this는 string 기본형이 아닌 String 객체 레퍼이다. String 객체를 직접 생성할 수도 있으며, string 기본형처럼 동작한다.

> x = "hello"
> x.language = "English"
> x.language
undefined

어떤 속성을 기본형에 할당한다면 그 속성은 사라지는데, 실제로는 x가 String 객체로 변환된 후 language 속성이 추가되었고 language 속성이 추가된 객체가 버려진 것.

타입스크립트는 기본형과 객체 레퍼 타입을 별도로 모델링한다.

  • string과 String
  • number와 Number
  • boolean과 Boolean

string은 String에 할당 할 수 있지만 String은 string에 할당할 수 없다. 대부분의 라이브러리와 마찬가지로 타입스크립트가 제공하는 타입 선언은 전부 기본형 타입으로 되어 있다.

타입스크립트는 기본형 타입을 객체 레퍼에 할당하는 선언을 허용한다. 그러나 기본형 타입을 객체 레퍼에 할당하는 구문은 오해하기 쉽고 굳이 그렇게 할 필요도 없기 때문에, 그냥 기본형 타입을 사용해라!