diff --git "a/4\354\243\274\354\260\250/\354\236\245\354\247\200\354\235\200/item28~37.md" "b/4\354\243\274\354\260\250/\354\236\245\354\247\200\354\235\200/item28~37.md" new file mode 100644 index 0000000..762ef7f --- /dev/null +++ "b/4\354\243\274\354\260\250/\354\236\245\354\247\200\354\235\200/item28~37.md" @@ -0,0 +1,864 @@ +# 4장. 타입 설계 + +## item28. 유효한 상태만 표현하는 타입을 지향하기 + +타입을 잘 설계하면 코드를 직관적으로 작성할 수 있다. + +효과적으로 타입을 설계하려면 유효한 상태만 표현할 수 있는 타입을 만들어내는 것이 좋다. + +- 무효한 상태를 포함하도록 타입설계한 CASE + + ``` + interface State { + pageText: string; + isLoading: boolean; // loading 상태 + error?: string; // 에러 메시지 + } + + function renderPage(state: State) { + if (state.error) { + return
{state.error}
; // ❓🤷‍♀️ - isLoading도 true, error도 true이면? + } else if (state.isLoading) { + return
Loading...
; + } + return
{state.pageText}
; + } + ``` + + isLoading과 error 두가지 속성이 충돌한다. (오류이면서 동시에 로딩중일 수 있음 == 무효한 상태) + + 이런 무효한 상태가 존재한다면 제대로 구현할 수 없다. + +- 무효한 상태 X인 CASE + ```tsx + interface RequestPending { + state: "pending"; + } + interface RequestSuccess { + state: "ok"; + pageText: string; + } + interface RequestError { + state: "error"; + error: string; + } + type RequestState = RequestPending | RequestSuccess | RequestError; // 태그된 유니온 타입 + + interface State { + currentPage: string; + requests: { + [url: string]: RequestState; // url별로 요청 상태를 저장 + }; + } + + function renderPage(state: State) { + const { currentPage } = state; + const currentRequest = state.requests[currentPage]; + if (currentRequest.state === "pending") { + return
Loading...
; + } else if (currentRequest.state === "ok") { + return
{currentRequest.pageText}
; + } else { + return
{currentRequest.error}
; + } + } + ``` + state가 currentRequest.state 하나로 관리되기 때문에, 무효한 상태를 허용하지 않도록 개선되었다. + 코드가 길어지거나 표현하기 어려울 수 있지만, 시간을 줄일 수 있기때문에 이 방식을 채택해야한다. + +--- + +## item29. 사용할 때는 너그럽게, 생성할 때는 엄격하게 + +함수의 매개변수는 타입의 범위가 넓어도 되지만, 결과를 반환할 때는 타입의 범위가 더 구체적이여야 한다. + +### 자유로운 타입의 매개변수 받기 + +```tsx +declare function setCamera(camera: CameraOptions): void; +declare function viewportForBounds(bounds: LngLatBounds): CameraOptions; + +// 자유로운 타입의 매개변수 +interface CameraOptions { + center?: LngLat; + zoom?: number; + bearing?: number; + pitch?: number; +} + +type LngLat = + | { lat: number; lng: number } + | { lon: number; lat: number } + | [number, number]; + +type LngLatBounds = + | { northeast: LngLat; southwest: LngLat } + | [LngLat, LngLat] + | [number, number, number, number]; +``` + +- CameraOptions의 값은 모두 선택적이어서, 값을 건드리지 않으며 다른 값 설정 가능 +- LngLat타입도 `{lng, lat}`, `{lon, lat}`, `[lng,lot]` 모두 넣을수 있어 편리함 + +### 생성될 때의 반환타입도 너무 자유롭다면? + +```tsx +declare function viewportForBounds(bounds: LngLatBounds): CameraOptions; + +function focusOnFeature(f: Feature) { + const bounds = calculateBoundingBox(f); + const camera = viewportForBounds(bounds); // 👎 반환 타입이 CameraOptions로, 너무 자유로움! + setCamera(camera); + + const { + center: { lat, lng }, // ❌ Property 'lat' does not exist on type 'LangLat | undefined' + zoom, + } = camera; + + zoom; // 👎 타입이 number | undefined + window.location.search = `?lat=${lat}&lng=${lng}&zoom=${zoom}`; +} +``` + +- CameraOption의 center속성이 선택적이기 때문에, center의 타입이 `LangLat | undefined` +- zoom 또한 `number|undefined`로 추론됨 + +⇒ viewportForBounds의 타입선언이 사용될 때 뿐 아니라 만들어질 때도 너무 자유로운것이 문제를 일으킨다. + +⇒ 매개변수의 타입이 넓으면 사용하기 편리하지만, 반환타입의 범위가 넓으면 불편하다. 따라서 반환타입을 엄격하게 적용해야한다. + +### 생성될 때의 반환타입을 엄격하게 적용하기 + +유니온 타입의 요소별 분기를 위한 방법은, 기본 형식을 따로 구분하는 것이다. + +기본 형태를 반환타입으로 쓰고, 느슨한 형태를 매개변수타입으로 사용하는게 좋다. + +```tsx +// 좌표의 기본 형식 +interface LngLat { + lat: number; + lng: number; +} +// 더 자유로운 형식 포함한 느슨한 타입 +type LngLatLike = LngLat | { lon: number; lat: number } | [number, number]; +type LngLatBounds = + | { northeast: LngLatLike; southwest: LngLatLike } + | [LngLatLike, LngLatLike] + | [number, number, number, number]; +``` + +- “배열같은것”의 사용을 위해 ArrayLike를 사용했던 것 처럼, `LngLat`과 `LngLatLike`로 구분 + - 🤖GPT설명 - Type & TypeLike + TypeScript에서 Type과 TypeLike 패턴은 타입 시스템의 중요한 설계 관례입니다. + - **기본관례** + 1. **기본 타입 (Type)**: + - 정확하고 엄격한 구조를 가진 타입 + - 명확한 속성과 동작을 정의 + - 일관된 인터페이스 제공 + 2. **유연한 타입 (TypeLike)**: + - 기본 타입을 포함하는 유니온 타입 + - 유사하지만 구조가 조금 다른 여러 형태 허용 + - 더 넓은 범위의 입력값 수용 + - **주요 특징** + - **호환성**: TypeLike는 항상 Type과 호환됨 (Type은 TypeLike의 부분집합) + - **방향성**: 엄격한 내부 구현 → 유연한 외부 API + - **사용 문맥**: 주로 함수 매개변수에 TypeLike, 반환 값에 Type 사용 + - **자주 사용되는 예시** + 1. **Array와 ArrayLike** + + ```tsx + // Array: 모든 배열 메서드 포함* + const arr: Array = [1, 2, 3]; + // ArrayLike: length와 인덱스 접근만 가능* + const arrLike: ArrayLike = { 0: 1, 1: 2, 2: 3, length: 3 }; + ``` + + 2. **Promise와 PromiseLike** + + ```tsx + // Promise: 완전한 Promise API + const p: Promise = new Promise((resolve) => resolve("done")); + + // PromiseLike: then 메서드만 있으면 됨 + const pLike: PromiseLike = { + then(onfulfilled) { + return onfulfilled("done"); + }, + }; + ``` + - 참고: https://jaenny-dev.tistory.com/5 +- LngLatBounds를 생성하는 다른 함수가 있다면, `LngLatBounds`와 `LngLatBoundsLike`로 구분하면 됨 + +```tsx +// Camera의 기본 형식 +interface Camera { + center: LngLat; + zoom: number; + bearing: number; + pitch: number; +} +// 더 자유로운 형식을 포함한 느슨한 타입 +interface CameraOptions extends Omit, "center"> { + center?: LngLatLike; +} +``` + +- 완벽하게 정의된 `Camera`와, 부분적으로 정의된 `CameraOptions`를 구분 + - 🤖GPT설명 -  CameraOptions extends Omit, "center"> + 1. Partial + - Partial은 타입의 모든 속성을 선택적(optional)으로 만듭니다. + - Camera 인터페이스의 모든 속성(center, zoom, bearing, pitch)이 선택적이 됩니다. + ```tsx + { + center?: LngLat; + zoom?: number; + bearing?: number; + pitch?: number; + } + ``` + 2. Omit, "center"> + - Omit은 타입에서 특정 속성을 제외합니다. + - Partial에서 "center" 속성을 제외합니다. + ```tsx + { + zoom?: number; + bearing?: number; + pitch?: number; + } + ``` + 3. interface CameraOptions extends Omit, "center"> + - Camera 인터페이스의 대부분의 속성을 상속받되, center 속성만 특별하게 처리합니다. + - 모든 속성이 선택적(optional)입니다. + ```tsx + { + center?: LngLatLike; // 선택적 + zoom?: number; // 선택적 + bearing?: number; // 선택적 + pitch?: number; // 선택적 + } + ``` + +```tsx +declare function setCamera(camera: CameraOptions): void; +declare function viewportForBounds(bounds: LngLatBounds): Camera; // 👍 반환타입을 Camera로 더 엄격하게! + +function focusOnFeature(f: Feature) { + const bounds = calculateBoundingBox(f); + const camera = viewportForBounds(bounds); // ✅ 반환타입이 Camera + setCamera(camera); // Camera타입은 CameraOptions타입에 할당 가능 + + const { + center: { lat, lng }, // ✅ 정상 + zoom, + } = camera; + zoom; // 👍 타입이 number + window.location.search = `?lat=${lat}&lng=${lng}&zoom=${zoom}`; +} +``` + +--- + +## item30. 문서에 타입 정보를 쓰지 않기 + +주석에 변수명과 타입정보를 적지 말아야 한다. 타입 정보에 모순이 발생할 수 있다. + +- 주석 + + ```tsx + /** + * 페이지에 따른 전경색 반환 + * 매개변수는 0또는 1 // 아님 + * 매개변수가 없을땐 표준 전경색 문자열 반환 // 아님 + */ + function getForegroundColor(page: string) { + return page == "login" ? { r: 255, g: 255, b: 255 } : { r: 0, g: 0, b: 0 }; + } + ``` + + - 강제하지 않는 이상 주석은 코드와 동기화되지 않는다. + +- 코드로 작성 + ```tsx + /** + * 페이지에 따른 전경색 반환 + */ + function getForegroundColor(page: string): Color { + return page == "login" ? { r: 255, g: 255, b: 255 } : { r: 0, g: 0, b: 0 }; + } + ``` + - 타입구문은 컴파일러가 체크해주기 때문에 구현체와 정합성에 어긋나지 않는다. 따라서 코드가 추후 변경되더라도 정보가 동기화되기때문에 유지보수에 용이하다. + ++) 변수명에도 타입을 넣지 않는게 좋다. (`ageNum`❌, `age: number`✅) + +--- + +## item31. 타입 주변에 null값 배치하기 + +### 함수 반환 타입 전체가 Null이거나 Null이 아니게 만들기 + +값이 전부 null이거나, 전부 null이 아닌 경우로 분명히 구분된다면 값이 섞여있을 때 보다 다루기 쉽다. + +따라서 반환타입을 더 큰 객체로 만들고 반환타입 전체가 null이거나 null이 아니게 만들어야 한다. + +- 반환타입에 값이 섞여있는 경우 + + ```tsx + function extent(nums: number[]) { + let min, max; + for (const num of nums) { + if (!min) { + min = num; + max = num; + } else { + min = Math.min(min, num); + max = Math.max(max, num); // ❌ max의 타입이 number | undefined -> undefined일때 오류 발생함 + } + } + return [min, max]; // 👎 반환타입이 (number | undefined)[] + } + + const [min, max] = extent([0, 1, 2]); + const span = max - min; // ❌ max/min이 undefined일때 오류 발생함 + ``` + + 반환타입에 number와 undefined가 섞여있게된다. 이럴 경우 객체를 사용하는 곳 에서 에러를 발생시킬 수 있다. + +- 반환타입이 null이거나 null이 아님 + ```tsx + function extent2(nums: number[]) { + let result: [number, number] | null = null; + for (const num of nums) { + if (!result) { + result = [num, num]; + } else { + result = [Math.min(result[0], num), Math.max(result[1], num)]; + } + } + return result; // 👍 반환타입이 [number, number] | null + } + + const [min2, max2] = extent2([0, 1, 2])!; // null아님 단언(!) 을 사용하여 null 제외하기 + const span2 = max2 - min2; *// ✅ 오류 발생하지 않음* + ``` + 반환타입이 null이거나 null이 아니게 되어, 결과값으로 단일 객체를 사용할 수 있다. + +### 클래스에서 값이 모두 준비되었을 때 값 생성하기 + +- 데이터를 따로 update + + ```tsx + class UserPosts { + user: UserInfo | null; + posts: Post[] | null; + constructor() { + this.user = null; + this.posts = null; + } + + async init(userId: string) { + return Promise.all([ + async () => (this.user = await fetchUser(userId)), + async () => (this.posts = await fetchPosts(userId)), // 👎 user와 posts가 따로 업데이트 됨 + ]); + } + + getUserName() { + // 👎 user와 posts가 각각 null이거나 null아닐 수 있음 (경우의 수 4가지) + return this.user.name; + } + } + ``` + + 속성값 두가지가 null이거나 null이 아닐 수 있어, 총 4가지의 경우의 수가 존재한다. + + 이 속성값의 불확실성은 null체크를 난무하게 만들어 버그를 양산하게 된다. + +- 데이터가 모두 준비되었을 때 update + ```tsx + class UserPosts { + user: UserInfo | null; + posts: Post[] | null; + constructor() { + this.user = null; + this.posts = null; + } + + async init(userId: string): Promise { + const [user, posts] = await Promise.all([ + fetchUser(userId), + fetchPosts(userId), + ]); + return new UserPosts(user, posts); // 👍 user와 posts가 동시에 업데이트 됨 + } + + getUserName() { + // 👍 user와 posts가 모두 null이 아니라는 것을 확신할 수 있음 + return this.user.name; + } + } + ``` + 필요한 데이터를 한번에 업데이트하게 되면 모두 null이 아니게 된다. 이 경우엔 메서드 작성이 더 쉬워진다. + +--- + +## item32. 유니온의 인터페이스보다는 인터페이스의 유니온 사용하기 + +유니온의 인터페이스보다는, 인터페이스의 유니온이 더 정확하고 TS가 이해하기 좋다. + +### 인터페이스의 유니온으로 속성간의 관계 명확히 하기 + +- 유니온의 인터페이스 + + ```tsx + interface Layer { + type: "fill" | "line" | "point"; + layout: FillLayout | LineLayout | PointLayout; + paint: FillPaint | LinePaint | PointPaint; + } + ``` + + 만약 layout이 LineLayout이면서, pain가 FillPaint인것은 말이 되지 않는다. + + 이는 라이브러리에서 오류를 일으키고 인터페이스를 다루기 어렵게 만든다. + +- 인터페이스의 유니온 + ```tsx + interface FillLayer extends Layer { + type: "fill"; + layout: FillLayout; + paint: FillPaint; + } + interface LineLayer extends Layer { + type: "line"; + layout: LineLayout; + paint: LinePaint; + } + interface PointLayer extends Layer { + type: "point"; + layout: PointLayout; + paint: PointPaint; + } + type Layer = FillLayer | LineLayer | PointLayer; + ``` + 유효한 인터페이스의 유니온으로 Layer를 정의하면 속성들이 잘못된 조합으로 섞이는 경우를 방지할 수 있고, 유효한 상태만을 표기할 수 있다. + 이러한 타입정의를 통해 속성관의 관계를 더 명확히 만들 수 있다. + +--- + +## item33. string 타입보다 더 구체적인 타입 사용하기 + +### 문자열을 남발하여 선언된 코드 피하기 + +string 타입은 ‘X’부터 모비딕 전체내용 까지, 그 범위가 매우 넓다. 단순히 string을 이용하기보다는 더 좁은 타입이 적절하지 않을지 고민해보아야 한다. + +- 문자열을 남발하여 선언된 예시 (Stringly Typed) + + ```tsx + interface Album { + artist: string; + title: string; + releaseDate: string; // "2024-01-01" + recordingType: string; // "live" 혹은 "studio" + } + const album: Album = { + artist: "The Beatles", + title: "Abbey Road", + releaseDate: "September 26, 1969", // ❗️ 형식이 다름 - TS에서 잡을 수 없음 + recordingType: "Studio", // ❗️ "S" 오타 - TS에서 잡을 수 없음 + }; + ``` + + 포맷이 정해져있는 releaseDate와 “live”혹은 “studio”값만 허용하는 recordingType까지 string으로 지정되었다. + + 다음과 같은 경우에는 의도에 맞지 않는 값이 들어와도 체크할 수 없다. + +- 타입의 범위를 좁힌 예시 + ```tsx + type RecordingType = "live" | "studio"; // 👍 두개의 유니온타입으로 제한 + interface Album { + artist: string; + title: string; + releaseDate: Date; // 👍 Date형식으로 제한 + recordingType: RecordingType; + } + const album: Album = { + artist: "The Beatles", + title: "Abbey Road", + releaseDate: new Date("1969-09-26"), + recordingType: "studio", + }; + ``` + 타입을 Date와 특정 문자열의 Union타입으로 제한하였다. 이렇게 바꾸면 TS는 오류를 더 세밀하게 체크할 수 있다. + +### string 타입 범위를 좁게 한 장점 + +1️⃣ 타입을 명시적으로 정의하여 다른 곳으로 값이 전달되어도 타입정보가 유지된다. + +```tsx +function getAlbumsOfType(recordingType: string): Album[] { + // 호출하는 곳에서 string타입이어야 한다는것 외에 다른 정보가 없음 + return albums.filter((album) => album.recordingType === recordingType); +} +``` + +"live" 혹은 "studio"라는 정보는 Album의 정의 안에 숨어있고, 함수를 호출하는 쪽에서는 recordingType에 대해 몰라도 된다. + +2️⃣ 타입을 명시적으로 정의하고 해당 타입의 의미를 설명하는 주석을 붙여넣을 수 있다. + +```tsx +/** 이 녹음은 어떤 환경에서 이루어졌는지? */ +type RecordingType = "live" | "studio"; + +function getAlbumsOfType(recordingType: RecordingType): Album[] { + // type RecordingType = "live" | "studio" - 이 녹음은 어떤 환경에서 이루어졌는지? (👍 주석 확인 가능!) + return albums.filter((album) => album.recordingType === recordingType); +} +``` + +타입을 `RecordingType`으로 바꾸면, 함수를 사용하는 쪽에서 "live" | "studio" 라는 정보와 주석을 확인할 수 있다. + +3️⃣ keyof연산자로 더 세밀하게 객체의 속성 체크가 가능하다. + +- any사용 + + ```tsx + // 어떤 배열에서 한 필드의 값만 추출하는 함수 + function pluck(records: any[], key: string): any[] { + return records.map((record) => record[key]); + } + ``` + + 에러가 나진 않지만, key에 모든 string타입이 허용되고, 리턴타입도 any타입이어서 정밀하지 못하다. + +- 제네릭타입 & keyof 사용 + + ```tsx + // keyof T == "artist" | "title" | "releaseDate" | "recordingType" + function pluck(records: T[], key: keyof T): T[keyof T][] { + return records.map((record) => record[key]); + } + + // releaseDates: (string | Date)[] - 👎 Album객체에서 가질 수 있는 모든 타입 + const releaseDates = pluck(albums, "releaseDate"); + ``` + + key가 string타입에서 keyof T타입으로 바뀌었기 때문에, Album 키값들의 Union으로 제한된다. + + 하지만 반환타입을 찍어보면, Album객체 내에서 가질 수 있는 모든 값의 타입이 찍혀나오고, 이 또한 범위가 너무 넓어서 적절하지 않다. + +- 두번째 제네릭 K 도입 + ```tsx + function pluck(records: T[], key: K): T[K][] { + return records.map((record) => record[key]); + } + + // 👍 releaseDates: Date[] + const releaseDates = pluck(albums, "releaseDate"); + ``` + keyof T를 extends한 K제네릭을 도입하면, 반환타입의 값을 적절하게 줄일 수 있다. + 이렇게 객체 속성이름을 함수의 매개변수로 받는 경우엔 keyof를 활용한 제네릭을 사용해야한다. + +--- + +## item34. 부정확한 타입보다는 미완성 타입을 사용하기 + +일반적으론 타입이 구체적일수록 버그를 더 많이 잡고 TS가 제공하는 도구를 활용할 수 있지만, 타입 선언의 정밀도를 높이는 일은 주의를 기울여야 한다. + +잘못된 타입은 차라리 타입이 없는 것 보다 못할 수 있다! + +- 수정 전 - 미완성의 타입 + + ```tsx + interface Point { + type: "Point"; + coordinates: number[]; + } + interface LineString { + type: "LineString"; + coordinates: number[][]; + } + interface Polygon { + type: "Polygon"; + coordinates: number[][][]; + } + type Geometry = Point | LineString | Polygon; + ``` + + 이 좌표에 쓰이는 number[]가 추상적이라, 이 coordinates를 경도와 위도를 나타내는 튜플타입 선언으로 수정한다. + +- 수정 후 - 부정확한 타입 + ```tsx + type GeoPosition = [number, number]; + interface Point { + type: "Point"; + coordinates: GeoPosition; + } + ``` + 경도와 위도를 나타내는 number의 튜플타입으로 지정하여 더 정확한 표현이 가능해졌지만, + 만약 세번째 요소인 고도를 사용하는 사용자가 있었다면 이는 에러를 유발시킨다. + 이렇게 타입을 세밀하게 만들고자 하는 시도가 과해지면 타입을 오히려 부정확하게 만들수 있다. + + + +--- + +## item35. 데이터가 아닌, api와 명세를 보고 타입 만들기 + +### 명세 참고하기 + +타입을 생성할 때, 예시 데이터가 아니라 명세를 참고하여 타입을 생성해야한다. + +```tsx +// geojson +interface BoundingBox { + lat: [number, number]; + lng: [number, number]; +} +import { Feature } from "geojson"; + +function calculateBoundingBox(f: Feature): BoundingBox | null { + let box: BoundingBox | null = null; + + const helper = (coords: any[]) => { + // ... + }; + + const { geometry } = f; + if (geometry) { + helper(geometry.coordinates); // ❌ Property 'coordinates' does not exist on type 'Geometry'. + } + + return box; +} +``` + +위 예시는 geometry에 coordinates 속성이 있다고 가정한 것이 문제다. +다른 도형과 다르게 GeometryCollection에는 coordinates 속성이 없다. + +```tsx +function calculateBoundingBox(f: Feature): BoundingBox | null { + let box: BoundingBox | null = null; + const helper = (coords: any[]) => { + // ... + }; + + const geometryHelper = (geometry: Geometry) => { + // 👍 모든 타입 지원하는 헬퍼함수 + if (geometry.type === "GeometryCollection") { + geometry.geometries.forEach(geometryHelper); + } else { + helper(geometry.coordinates); // OK + } + }; + const { geometry } = f; + if (geometry) { + geometryHelper(geometry); + } + + return box; +} +``` + +위와 같이 GeometryCollection 조건을 분기해서 헬퍼 함수를 호출하면 모든 타입을 지원할 수 있다. + + + +### GraphQL + +GraphQL의 장점은 특정 쿼리에 대해 타입스크립트 타입을 생성할 수 있다는 것이다. + +타입은 단 하나의 원천 정보인 GraphQL 스키마로 부터 생성이 되기 때문에, 타입과 실제 값이 항상 일체한다. + +만약 명세나 공식 스키마가 없다면, 데이터를 기반으로 타입을 생성해야하고, quicktype같은 도구를 이용할 수도 있다. 하지만 생성된 타입이 실제 데이터와 일치하지 않을 수 있으므로 주의해야한다. +https://app.quicktype.io/ + +--- + +## item36. 해당 분야의 용어로 타입 이름 짓기 + +엄선된 타입, 속성, 변수의 이름은 의도를 명확히 하고 코드와 타입의 추상화 수준을 높여준다. + +반면에 잘못 선택한 타입 이름은 코드의 의도를 왜곡하고 잘못된 개념을 심어줄 수 있다. + +- 잘못된 예시 + + ```tsx + interface Animal { + name: string; + endangered: boolean; + habitat: string; + } + ``` + + - name이 너무 일반적인 용어라 명확하지 않다. + - endangered 속성이 boolean타입이라 애매한 영역이 생긴다 (이미 멸종된 동물은 true?) + - habitat은 속성 범위가 너무 넓은 string타입이다. + +- 개선 + ```tsx + interface Animal { + commonName: string; + genus: string; + species: string; + status: ConservationStatus; + climates: KoppenClimate; + } + type ConservationStatus = "EX" | "EW" | "CR" | "EN" | "VU" | "NT" | "LC"; + type KoppenClimate = "Af" | "Am" | "As" | "Aw"; + ``` + - name을 commonName, genus, species 등 구체적인 용어로 대체했다. + - endangered는 IUCN 표준 분류체계인 ConservationStatus타입으로 변경되었다. + - habitat은 기후를 뜻하는 climates로 변경되었고, 쾨펜 기후분류를 사용했다. + + + +--- + +## item37. 공식 명칭에는 상표를 붙이기 + +### 상표 사용하기 + +- 구조적 타이핑에서 발생하는 에러 + + ```tsx + interface Vector2D { + x: number; + y: number; + } + function calculateNorm(p: Vector2D) { + return Math.sqrt(p.x * p.x + p.y * p.y); + } + + calculateNorm({ x: 3, y: 4 }); // ✅ 정상, 5 + const vector3d = { x: 3, y: 4, z: 5 }; // 추가된 속성 z + calculateNorm(vector3d); // ✅ 정상⁉️ 하지만 calculateNorm은 3차원 벡터의 계산식이 아님! + ``` + + 구조적 타이핑 특성 때문에 가끔 코드가 이상한 결과를 낼 수 있다. + + 해당 코드는 구조적 타이핑 관점에서 문제를 일으키지 않지만, 수학적으로 따지면 옳지 않은 값을 리턴한다. + + `calculateNorm`이 3차원 벡터를 허용하지 않하는 방법중 하나는 바로 상표(`_brand`)를 사용하는 방식이다 + +- 생성 함수를 통해 상표(\_brand) 붙이기 + ```tsx + interface Vector2D { + _brand: "2d"; // 공식명칭(nominal typing) - 리터럴타입 "2d" + x: number; + y: number; + } + + // ✨ 생성 함수 + function createVec2(x: number, y: number): Vector2D { + return { x, y, _brand: "2d" }; + } + + // 계산 함수 + function calculateNorm(p: Vector2D) { + return Math.sqrt(p.x * p.x + p.y * p.y); + } + + calculateNorm(createVec2(3, 4)); // ✅ 정상, 생성자 함수를 사용하여 Vector2D 보장 + + const vec3D = { x: 3, y: 4, z: 5 }; + calculateNorm(vec3D); // ❌ 오류 - Property '_brand' is missing in type '{ x: number; y: number; z: number; }' but required in type 'Vector2D' + + const vec2D = { x: 3, y: 4, _brand: "2d" }; // _brand가 string타입으로 추론됨 + calculateNorm(vec2D); // ❌오류 - Types of property '_brand' are incompatible. Type 'string' is not assignable to type '"2d"'. + ``` + `_brand`는 리터럴타입의 ”2d”로, 해당 타입의 고유성을 보장하는 역할을 하고 일반 string과는 구분된다. + `createVec2`생성자 함수를 이용하여 `_brand`속성을 부여하면, `Vector2D`라는 타입의 고유성이 보장되고, 구조적 타이핑에 의한 의도치 않은 동작을 방지할 수 있다. + +### 타입가드 함수로 런타임에 상표(\_brand) 검사하기 + +``` +// 절대경로 판단하기 + +type AbsolutePath = string & { _brand: "absolutePath" }; // string과 브랜드 속성의 교차타입 +function listAbsolutePath(path: AbsolutePath) { + return path; +} + +// 🛡️ 타입 가드 함수 +function isAbsolutePath(path: string): path is AbsolutePath { + return path.startsWith("/"); +} + +function f(path: string) { + if (isAbsolutePath(path)) { + // 조건문으로 타입 정제 + listAbsolutePath(path); // ✅ 정상 (path는 AbsolutePath 타입) + } + + listAbsolutePath(path); // ❌ path는 string타입이므로 오류 + listAbsolutePath(path as AbsolutePath); // ✅ 정상 (path는 AbsolutePath 타입으로 강제 형변환) - ❗️하지만 단언문은 지양해야함 + +} +``` + +AbsolutePath는 string 타입과 브랜드 속성의 교차 타입이고, 이런 객체를 만들 순 없기에 온전히 타입시스템의 영역이다. + +타입가드 함수 `isAbsolutePath`를 if문에 활용하면 런타임 검사를 통해 타입정제가 가능하다. 이 타입가드를 통해 조건문 안에선 안정적으로 타입을 사용할 수 있다. + +로직을 분기하는 대신 as AbsolutePath를 사용해 오류를 제거할 수도 있지만, 타입 단언의 사용은 지양해야한다. + +```tsx +// 이진 검색 + +type SortedList = T[] & { _brand: "sorted" }; + +// 🛡️ 타입 가드 함수 +function isSorted(xs: T[]): xs is SortedList { + for (let i = 0; i < xs.length - 1; i++) { + if (xs[i] > xs[i + 1]) { + return false; + } + } + return true; +} +// 사용 함수 +function binarySearch(xs: SortedList, x: T): boolean { + // 이진검색 - 이미 정려 + let low = 0; + let high = xs.length - 1; + + while (low <= high) { + const mid = Math.floor((low + high) / 2); + const midItem = xs[mid]; + if (midItem === x) return true; + [low, high] = midItem < x ? [mid + 1, high] : [low, mid - 1]; + } + return false; +} + +function isFoundNum(arr: number[]) { + if (isSorted(arr)) { + return binarySearch(arr, 3); + } +} +``` + +이진검색`binarySearch`함수의 경우 이미 정렬된 상태를 가정하기 때문에, 정렬되어있지 않은 배열이 들어온다면 잘못된 결과가 나올 수 있다. + +타입스크립트에서 목록이 정렬되어있다는 의도를 표현하려면 상표기법을 사용할 수 있다. + +`binarySearch`를 호출하려면 `isSorted`함수를 통해 `SortedList`타입임을 증명해야한다. diff --git "a/6\354\243\274\354\260\250/\353\260\234\355\221\234\354\236\220\353\243\214/\353\260\234\355\221\234\354\236\220\353\243\214.md" "b/6\354\243\274\354\260\250/\353\260\234\355\221\234\354\236\220\353\243\214/\353\260\234\355\221\234\354\236\220\353\243\214.md" index f585162..98244fa 100644 --- "a/6\354\243\274\354\260\250/\353\260\234\355\221\234\354\236\220\353\243\214/\353\260\234\355\221\234\354\236\220\353\243\214.md" +++ "b/6\354\243\274\354\260\250/\353\260\234\355\221\234\354\236\220\353\243\214/\353\260\234\355\221\234\354\236\220\353\243\214.md" @@ -1,10 +1,14 @@ +# 7장. 코드를 작성하고 실행하기 + ## Item53. 타입스크립트 기능보다는 ECMAScript기능을 사용하기 -자바스크립트에 새로 추가된 기능은 타입스크립트 초기 버전의 기능과 호환성 문제를 발생시켰다. +TS가 태동하던 2010년경, JS는 결함과 개선사항이 많은 언어였고 클래스, 데코레이터 등의 기능은 프레임워크나 트랜스파일러로 보완하는게 일반적이었다. 따라서 TS도 초기버전엔 독립적으로 개발한 클래스, 열거형(enum), 모듈시스템을 포함시킬 수 밖에 없었다. + +이후 TC39는 이런 내장기능들을 추가했고, 자바스크립트에 새로 추가된 기능은 타입스크립트 초기 버전의 기능과 호환성 문제를 발생시켰다. 타입스크립트 진영은 JS 신규 기능을 그대로 채택하고, TS 초기버전과의 호환성을 포기했다. -이 전에 사용되고 있던 타입공간(TS)와 값공간(JS)의 경계를 혼란스럽게 만드는 기능들이 있는데, 이는 사용하지 않는것이 좋다. +초기에 사용되고 있던 타입공간(TS)와 값공간(JS)의 경계를 혼란스럽게 만드는 기능들이 있는데, 이는 사용하지 않는것이 좋다. ### 열거형(enum) @@ -19,12 +23,12 @@ let flavor = Flavor.CHOCOLATE; // flavor: Flavor Flavor[0]; // 값이 VANILLA ``` -타입스크립트 열거형은 상황에 따라 다르게 동작한다. +타입스크립트 열거형은 상황에 따라 다르게 동작하고 몇가지 문제점이 있다. - 숫자 열거형(Flavor) 에 0,1,2 외의 다른 숫자가 할당되면 위험하다. (원래 비트 플래그 구조를 표현하기 위해 설계되어서) -→ `let flavor: Flavor = 3;` 같은 케이스는 막힘! (업데이트 된듯) -- 상수 열거형은 런타임에 완전히 제거된다. 앞의 예제를 const enum Flavor로 바꾸면 컴파일러는 Flavor.CHOCOLATE을 0으로 바꿔버린다. -- preserveConstEnums 플래그를 설정한 상태의 상수 열거형은 보통의 열거형처럼 런타임 코드에 상수 열거형 정보를 유지한다. +→ `let flavor: Flavor = 3;` 같은 케이스는 TS에서 에러로 표기됨 +- 상수 열거형은 런타임에 완전히 제거된다. 앞의 예제를 `const enum Flavor`로 바꾸면 컴파일러는 `Flavor.CHOCOLATE`을 0으로 바꿔버린다. +- `preserveConstEnums` 플래그를 설정한 상태의 상수 열거형은 보통의 열거형처럼 런타임 코드에 상수 열거형 정보를 유지한다. - 문자열 열거형은 런타임의 타입안정성과 투명성을 제공한다. 하지만 타입스크립트의 구조적 타이핑이 아닌 명목적 타이핑을 사용한다. ```tsx @@ -58,6 +62,33 @@ scoop(Flavor2.CHOCOLATE); ``` - 이렇게 열거형을 임포트하고 문자열 대신 사용해야한다. +- 🤖GPT 설명 - 명목적 타이핑이란? + + 타입스크립트는 기본적으로 **구조적 타이핑(Structural Typing)**을 사용합니다. 즉, 타입의 이름이 아니라 **구조(필드와 메서드의 형태)**를 기준으로 타입 호환성을 판단합니다. + + 하지만, **`enum`(열거형)**은 타입스크립트에서 명목적 타이핑처럼 동작합니다. 즉, 같은 구조를 가진 값이라도 열거형의 이름이 다르면 호환되지 않습니다. + + - **예제: 명목적 타이핑처럼 동작하는 `enum`** + + ```tsx + enum Flavor1 { + VANILLA = 'vanilla', + CHOCOLATE = 'chocolate', + } + + enum Flavor2 { + VANILLA = 'vanilla', + CHOCOLATE = 'chocolate', + } + + let flavor: Flavor1 = Flavor1.VANILLA; + + // ❌ 다른 열거형은 호환되지 않음 + flavor = Flavor2.VANILLA; // 오류: Type 'Flavor2' is not assignable to type 'Flavor1' + ``` + + - `Flavor1`과 `Flavor2`는 구조적으로 동일하지만, 타입스크립트는 **열거형의 이름**을 기준으로 타입 호환성을 판단합니다. + - 따라서, `Flavor1`과 `Flavor2`는 서로 다른 타입으로 간주됩니다. ```tsx // 👍 enum 대신 리터럴타입의 유니온 사용하기 @@ -69,15 +100,10 @@ flavor = 'mint' // ❌ Type '"mint"' is not assignable to type 'Flavor' enum을 사용하지 말고 리터럴 타입의 유니온을 사용해야 한다. -| **특징** | **enum** | **유니언 타입** | -| --- | --- | --- | -| **런타임 존재 여부** | 런타임에 객체로 존재 (Flavor.VANILLA) | 런타임에 제거됨 | -| **타입 안정성** | 명목적 타이핑 (정의된 열거형 값만 허용) | 구조적 타이핑 (정의된 문자열 값만 허용) | -| **가독성** | 열거형 이름으로 값의 의미를 명확히 표현 가능 | 문자열 값만 사용 | -| **성능** | 런타임에 객체를 유지 (숫자/문자열 열거형) | 런타임에 제거되어 더 가볍게 동작 | - ### 매개변수 속성 +보통 클래스를 초기화할 때 속성을 할당하기 위해 생성자의 매개변수를 사용하는데, TS에선 매개변수 속성이라는 더 간결한 문법을 제공한다. + - JS 문법 ```tsx @@ -234,7 +260,7 @@ function foo(abc: ABC) { ``` -- Object.entries는 복잡한 기교 없이 사용 가능하다. +- Object.entries는 직관적이진 않지만 복잡한 기교 없이 사용 가능하다. - 더욱 일반적으로 사용 가능