일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 이펙티브타입스크립트
- SemVer
- 캐나다개발자
- CSS방법론
- ASP.NET
- framer-motion
- 글또
- useState
- 개발자 원칙
- 타입스크립트
- 캐나다취준
- 코드트리
- 개발자를 위한 글쓰기 가이드
- framer
- 알고리즘
- Semantic Versioning
- 회고
- React-Router-Dom
- CSS
- react
- JSBridge
- JUNCTION2023
- 테오의 스프린트
- VS Code
- 시스템디자인
- Effective Typescript
- typescript
- Framer motion
- 글또 10기
- TS
- Today
- Total
큰 꿈은 파편이 크다!!⚡️
이펙티브 타입스크립트 🦅 2장 (1) 본문
2장 타입스크립트의 타입 시스템 (1)편에 들어가며
2장은 양이 생각보다 아주 매우 너무나 많아서 부득이하게 세 챕터로 나눠서 작성한다. (페이지 수가 1장의 거의 세 배)
6. 편집기를 사용하여 타입 시스템 탐색하기
TS를 설치하면 타입스크립트 컴파일러(tsc
), 단독으로 실행할 수 있는 TS서버(tsserver
)를 실행할 수 있다
- TS서버에서도 설정을 통해 편집기 자동완성, Go to Definition 등의 기능을 꼭 사용하자!
- 편집기에서 함수 또는 변수 위에 커서를 올리면 추론된 타입이 나타난다. 만약 기대한 타입과 다르다면 타입 선언을 직접 명시하자
7. 타입이 값들의 집합이라고 생각하기 🧐 이 아이템은 엄청 헷갈렸다..
- 코드가 실행되기 전, 즉 TS가 오류를 체크하는 순간에 변수는 타입을 가진다. 타입은 할당 가능한 값들의 집합 이며, 이 집합은 타입의 범위 라고도 부른다
- 가장 작은 집합은 아무 값도 포함하지 않는 공집합이며, TS에서는 never
타입이다
- never
타입 변수는 공집합이므로 아무런 값도 할당할 수 없다
const x: never = 12;
// ~ Type '12' is not assignable to type 'never'
- 그 다음으로 작은 집합은 한 가지 값만 포함하는 리터럴literal 타입이며, 유닛unit 타입이라고도 불린다
type A = 'A';
type B = 'B';
type Twelve = 12;
두 개 이상으로 묶으려면 유니온union 타입을 사용한다
- 유니온 타입은 값 집합들의 합집합이다
- 집합 관점에서 TS의 역할은 하나의 집합이 다른 집합의 부분 집합인지 검사하는 것이라고 볼 수 있다
type AB = 'A' | 'B';
type AB12 = 'A' | 'B' | 12;
type AB = 'A' | 'B';
type AB12 = 'A' | 'B' | 12;
const a: AB = 'A'; // OK, value 'A' is a member of the set {'A', 'B'}
const c: AB = 'C';
// ~ Type '"C"' is not assignable to type 'AB'
&
연산자는 두 타입의 인터섹션(intersection, 교집합)을 계산한다
- Person
과 Lifespan
인터페이스는 공통으로 가지는 속성이 없으므로 PersonSpan
을 공집합(never
타입)으로 예상하기 쉽지만, 타입 연산자는 인터페이스의 속성이 아닌, 값의 집합(타입의 범위)에 적용되며 추가적인 속성을 가지는 값도 그 타입에 속한다
- 즉, Person
과 Lifespan
둘 다 가지는 값은 인터섹션 타입에 속한다
- 인터섹션 타입의 값은 각 타입 내의 속성을 모두 포함한다
interface Identified {
id: string;
}
interface Person {
name: string;
}
interface Lifespan {
birth: Date;
death?: Date;
}
type PersonSpan = Person & Lifespan;
const ps: PersonSpan = {
name: 'Alan Turing',
birth: new Date('1912/06/23'),
death: new Date('1954/06/07'),
}; // OK
- 그러나 두 인터페이스의 유니온에서는 그렇지 않다
interface Identified {
id: string;
}
interface Person {
name: string;
}
interface Lifespan {
birth: Date;
death?: Date;
}
type PersonSpan = Person & Lifespan;
type K = keyof (Person | Lifespan); // Type is never
extends
를 통해 할당 가능한 부분 집합을 만들 수 있다
- Person을 상속한다는 의미를 집합의 관점에서 보면, PersonSpan
은 Person
의 부분 집합 범위를 가지는 어떤 타입이 된다
interface Person {
name: string;
}
interface PersonSpan extends Person {
birth: Date;
death?: Date;
}
8. 타입 공간과 값 공간의 심벌 구분
TS의 심벌Symbol은 타입 공간이나 값 공간 중의 한 곳에 존재하는데, 이름이 같더라도 속하는 공간에 따라 다른 것을 나타낼 수 있다
코드를 읽을 때 타입인지 값인지, 두 공간을 구분하려면 TS playground를 사용해보자. TS소스에서 변환된 JS결과물을 확인해보면 컴파일 과정에서 타입 정보는 제거되기 때문에, 심벌이 사라진다면 그것은 타입에 해당된다고 볼 수 있다.
예제
- interface Cylinder
에서 Cylinder
는 타입이지만, const Cylinder
에서의 Cylinder
는 값이다
- 일반적으로 type
/interface
뒤에 나오는 심벌은 타입이고, const
/let
뒤에 나오는 심벌은 값이다
- shape instanceof Cylinder
를 통해 shape
가 Cylinder
타입인지 체크하려고 했으나, instanceof
는 JS의 런타임 연산자이고 값에 대해 연산하기 때문에 오류가 발생한다. instanceof Cylinder
는 타입이 아닌 함수를 참조한다.
interface Cylinder {
radius: number;
height: number;
}
const Cylinder = (radius: number, height: number) => ({radius, height});
function calculateVolume(shape: unknown) {
if (shape instanceof Cylinder) {
shape.radius
// ~~~~~~ Property 'radius' does not exist on type '{}'
}
}
- 타입 선언(: Type
) 또는 단언문(as Type
) 뒤에 나오는 심벌은 타입이고, =
뒤에 나오는 모든 것은 값이다
- 클래스가 타입으로 쓰일 때는 형태(속성, 메소드)가 사용되는 반면, 값으로 쓰일 때는 생성자가 사용된다
예제
typeof
연산자는 타입에서 쓰일 때 TS 타입을 반환하지만 값의 관점에서는 JS런타임의 typeof
연산자가 되어 JS 런타임 타입을 반환한다
- const v = typeof Cylinder
는 클래스가 JS에서는 실제 함수로 구현되기 때문에 function이다
- type T = typeof Cylinder
에서 Cylinder
는 인스턴스의 타입이 아닌, new
키워드를 사용할 때 볼 수 있는 생성자 함수다.
class Cylinder {
radius=1;
height=1;
}
function calculateVolume(shape: unknown) {
if (shape instanceof Cylinder) {
shape // OK, type is Cylinder
shape.radius // OK, type is number
}
}
const v = typeof Cylinder; // Value is "function"
type T = typeof Cylinder; // Type is typeof Cylinder
타입 공간과 값 공간을 이해하지 못하고 작성한 TS코드는 잘 동작하지 않을 수 있다
예제 이 예제는 나도 평소 궁금했던 내용이다!
1) 단일 객체 매개변수를 받는 email 함수가 있다
interface Person {
first: string;
last: string;
}
function email(options: {person: Person, subject: string, body: string}) {
// ...
}
2) 객체 내의 각 속성을 로컬 변수로 만들어 주는 구조 분해destructuring 할당을 TS에서 사용해보면, 오류가 발생한다
- Person
, string
이 값의 관점에서 해석되기 때문이다
- 값의 관점에서 해석하면, Person
과 string
이라는 이름을 가지는 두 개의 변수를 생성하려고 시도하게 된다
function email({
person: Person,
// ~~~~~~ Binding element 'Person' implicitly has an 'any' type
subject: string,
// ~~~~~~ Duplicate identifier 'string'
// Binding element 'string' implicitly has an 'any' type
body: string}
// ~~~~~~ Duplicate identifier 'string'
// Binding element 'string' implicitly has an 'any' type
) { /* ... */ }
3) 문제 해결을 위해 타입과 값을 구분하여 작성한다
function email(
{person, subject, body}: {person: Person, subject: string, body: string}
) {
// ...
}
- 모든 값은 타입을 가지지만, 타입은 값을 가지지 않는다.
- type
과 interface
같은 키워드는 타입 공간에만 존재한다
- class
와 enum
등, 많은 연산자들과 키워드들은 타입 공간과 값 공간에서 다른 목적으로 사용될 수 있다
9. 타입 단언보다는 타입 선언 사용하기
- :
은 변수에 타입 선언을 붙여서 값이 선언된 타입임을 명시한다
- as
는 TS가 추론한 타입이 있더라도 타입 단언한 타입으로 간주하게 한다
interface Person { name: string };
const alice: Person = { name: 'Alice' }; // Type is Person
const bob = { name: 'Bob' } as Person; // Type is Person
- 타입 선언은 할당되는 값이 해당 인터페이스를 만족하는지 검사하여 오류를 표시하고, 타입 단언은 강제로 타입을 지정했으니 타입 체커에게 오류를 무시하라고 한다
- 타입 선언문에서는 잉여 속성 체크를 하지만, 단언문에서는 적용되지 않는다
interface Person { name: string };
const alice: Person = {};
// ~~~~~ Property 'name' is missing in type '{}'
// but required in type 'Person'
const bob = {} as Person; // No error
const alice2: Person = {
name: 'Alice',
occupation: 'TypeScript developer'
// ~~~~~~~~~ Object literal may only specify known properties
// and 'occupation' does not exist in type 'Person'
};
const bob2 = {
name: 'Bob',
occupation: 'JavaScript developer'
} as Person; // No error
- 화살표 함수의 타입 선언은 추론된 타입이 모호할 때가 있으므로 반환 타입을 잘 작성하자
interface Person { name: string };
const people: Person[] = ['alice', 'bob', 'jan'].map(
(name): Person => ({name})
);
타입 단언이 꼭 필요한 경우
- 타입 체커가 추론한 타입보다 개발자가 판단하는 타입이 더 정확할 때
- !
를 사용해서 null이 아님을 단언하는 경우
- 단언문은 컴파일 과정에서 제거되므로 null이 아님을 확신할 수 있을때 사용
- 확신할 수 없다면 null인 경우를 체크하는 조건문 사용
const elNull = document.getElementById('foo'); // Type is HTMLElement | null
const el = document.getElementById('foo')!; // Type is HTMLElement
10. 객체 래퍼 타입 피하기
"primitive".charAt(3)
- JS 기본형 type들은 메서드를 가지지 않는데, 위 예제를 보면 그중 하나인 string
은 메서드를 가진 것처럼 보인다. 사실 charAt
은 string
의 메서드가 아니며 string
'기본형'에는 메서드가 없는 것이 맞다. JS에는 String
객체 타입이 정의되어 있어 기본형과 객체 타입을 서로 자유롭게 변화한다. 위 경우 JS는 기본형을 String
객체로 래핑wrap하고, 메서드를 호출한 뒤, 래핑한 객체를 버린다
- String.prototype
을 몽키-패치monkey-patch한다면 동작을 확인할 수 있다
- 메서드 내의 this
는 String
객체 래퍼다.
// 실제로 이렇게 하지 마세요!
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));
- String
객체를 직접 생성할 수도 있으며, string
기본형처럼 동작하는 듯 하지만 항상 동일하게 동작하는 것은 아니다. String
객체는 오직 자기 자신과 동일하므로 객체 래퍼를 직접 생성해서 사용하지 말자
"hello" === new String("hello") //false
new String("hello") === new String("hello") //true
TS는 기본형과 객체 래퍼 타입을 별도로 모델링한다
- string/String, number/Number, boolean/Boolean, symbol/Symbol, bigint/BigInt
- 런타임의 값은 객체가 아닌 기본형이며, 기본형은 객체 래퍼에 할당될 수 있지만 반대는 불가능하다. 따라서 const s:String
으로 사용하는 것이 가능하지만 이렇게 할 필요가 없으니 지양하자
- new
없이 BigInt, Symbol을 호출하는 경우에는 기본형을 생성하므로 Symbol("sym")
과 같이 사용해도 좋다
마무리
이 글을 통해 새롭게 배운 내용은 이러하다.
- TS에서의 인터섹션과 유니온 원래 생각하던 개념과 반대로 생각..
- 타입 공간과 값 공간
- 타입 단언과 타입 선언, 그리고 언제 무엇을 사용해야 할지
- 객체 래퍼 타입과 동작 방식
낯설지만 TS를 작성하면서 한번쯤 궁금했던.. 그러나 굳이 찾아보지 않고 넘어간 내용들이 해소되었다.
'Web FE' 카테고리의 다른 글
이펙티브 타입스크립트 🦅 2장 (3) (0) | 2022.10.08 |
---|---|
이펙티브 타입스크립트 🦅 2장 (2) (0) | 2022.09.28 |
이펙티브 타입스크립트 🦅 1장 (0) | 2022.08.15 |
BEM: CSS 네이밍 방법론 (0) | 2022.08.06 |
🔨 웹에서 RTSP 스트리밍 + video autoplay 삽질기 (0) | 2022.07.23 |