[TypeScript 독학] #8 이펙티브 타입스크립트(2)

148126 단어 typescripttypescript

시작하며

지난글에 이어서 이펙티브 타입스크립트란 책을 읽으면서 중요하다고 생각하는 내용을 간단하게 정리해보려고 한다. 이번 내용은 다른 파트보다 내용이 많은 만큼 실무에서 변수들의 타입을 정의할 때 고민해야 하는 부분들에 대해 잘 다루고 있기 때문에, 한번 읽고 넘어가기 보다는 상황에 맞는 내용을 자주 찾아가면서 공부해야하는 책이라고 생각한다.



3장 타입 추론

타입을 정의하면 변수가 선언되는 시점에 잉여 속성체크가 작동한다.

function logProduct(product: Product) {
  const id: number = product.id;
     // ~~ Type 'string' is not assignable to type 'number'
  const name: string = product.name;
  const price: number = product.price;
  console.log(id, name, price);
}
const elmo: Product = {
  name: 'Tickle Me Elmo',
  id: '048188 627152',
//~~ (property) Product.id: number 'string' 형식은 'number' 형식에 할당할 수 없습니다.
  price: 28.99,
};

let과 const의 타입 추론

interface Vector3 { x: number; y: number; z: number; }
function getComponent(vector: Vector3, axis: 'x' | 'y' | 'z') {
  return vector[axis];
}
const x = 'x';  // type is "x"
let vec = {x: 10, y: 20, z: 30};
getComponent(vec, x);  // OK

let은 string 타입으로 추론하지만 const는 값이 변하지 않을 것이기 때문에 리터럴 타입으로 추론한다.


const v1 = {
  x: 1,
  y: 2,
};  // Type is { x: number; y: number; }

const v2 = {
  x: 1 as const,
  y: 2,
};  // Type is { x: 1; y: number; }

const v3 = {
  x: 1,
  y: 2,
} as const;  // Type is { readonly x: 1; readonly y: 2; }

as const를 사용하면 최대한 좁은 범위로 타입 추론한다.


타입좁히기

const el = document.getElementById('foo'); // Type is HTMLElement | null
if (!el) throw new Error('Unable to find #foo');
el; // Now type is HTMLElement
el.innerHTML = 'Party Time'.blink();
//if(!el)을 사용한 타입 좁히기의 예
function contains(text: string, search: string|RegExp) {
  if (search instanceof RegExp) {
    search  // Type is RegExp
    return !!search.exec(text);
  }
  search  // Type is string
  return text.includes(search);
}
//instanceof를 사용한 타입 좁히기의 예
interface A { a: number }
interface B { b: number }
function pickAB(ab: A | B) {
  if ('a' in ab) {
    ab // Type is A
  } else {
    ab // Type is B
  }
  ab // Type is A | B
}
//in을 사용한 타입 좁히기
const el = document.getElementById('foo'); // type is HTMLElement | null
if (typeof el === 'object') {
  el;  // Type is HTMLElement | null
}
//null의 타입도 object이기 때문에 타입 좁히기 실패
function foo(x?: number|string|null) {
  if (!x) {
    x;  // Type is string | number | null | undefined
  }
}
//0,''으로 인해 타입 좁히기 실패
interface UploadEvent { type: 'upload'; filename: string; contents: string }
interface DownloadEvent { type: 'download'; filename: string; }
type AppEvent = UploadEvent | DownloadEvent;

function handleEvent(e: AppEvent) {
  switch (e.type) {
    case 'download':
      e  // Type is DownloadEvent
      break;
    case 'upload':
      e;  // Type is UploadEvent
      break;
  }
}
//명시적으로 type을 명시하여(태그된 유니온) 타입 좁히기
function isInputElement(el: HTMLElement): el is HTMLInputElement {
  return 'value' in el;
}

function getElementContent(el: HTMLElement) {
  if (isInputElement(el)) {
    el; // Type is HTMLInputElement
    return el.value;
  }
  el; // Type is HTMLElement
  return el.textContent;
}
//사용자 정의 타입가드를 사용한 타입 좁히기

한꺼번에 객체 생성하기

interface Point { x: number; y: number; }
const pt = {} as Point;
pt.x = 3;
pt.y = 4;  // OK

//객체를 나누어서 정의해야 한다면 타입 단언문으로 가능
interface Point { x: number; y: number; }
const pt = {x: 3, y: 4};
const id = {name: 'Pythagoras'};
const namedPoint = {};
Object.assign(namedPoint, pt, id);
namedPoint.name;
        // ~~~~ Property 'name' does not exist on type '{}'

//namedPoint의 타입이 {}로 정해져서 할당 불가
interface Point { x: number; y: number; }
const pt = {x: 3, y: 4};
const id = {name: 'Pythagoras'};
const namedPoint = {...pt, ...id};
namedPoint.name;  // OK, type is string

//여러 객체를 합쳐야 하는 경우 전개연산자로 타입 정의
declare let hasMiddle: boolean;
const firstLast = {first: 'Harry', last: 'Truman'};
const president = {...firstLast, ...(hasMiddle ? {middle: 'S'} : {})};

//전개 연산자와 조건부 연산자를 사용하여 선택적 필드 생성하는 법
declare let hasMiddle: boolean;
const firstLast = {first: 'Harry', last: 'Truman'};
function addOptional<T extends object, U extends object>(
  a: T, b: U | null
): T & Partial<U> {
  return {...a, ...b};
}

const president = addOptional(firstLast, hasMiddle ? {middle: 'S'} : null);
president.middle  // OK, type is string | undefined

//헬퍼함수를 사용하여 선택적필드를 포함하는 새로운 객체 생성

일관성있는 별칭 사용하기

const borough = {name: 'Brooklyn', location: [40.688, -73.979]};
const loc = borough.location;

//객체의 요소에 별칭 부여
interface Coordinate {
  x: number;
  y: number;
}

interface BoundingBox {
  x: [number, number];
  y: [number, number];
}

interface Polygon {
  exterior: Coordinate[];
  holes: Coordinate[][];
  bbox?: BoundingBox;
}
function isPointInPolygon(polygon: Polygon, pt: Coordinate) {
  const box = polygon.bbox;
  if (polygon.bbox) {
    if (pt.x < box.x[0] || pt.x > box.x[1] ||
        //     ~~~                ~~~  Object is possibly 'undefined'
        pt.y < box.y[1] || pt.y > box.y[1]) {
        //     ~~~                ~~~  Object is possibly 'undefined'
      return false;
    }
  }
  // ...
}

//반복적으로 사용하는 값 polygon.bbox에 별칭(box) 부여
function isPointInPolygon(polygon: Polygon, pt: Coordinate) {
  const {bbox} = polygon;
  if (bbox) {
    const {x, y} = bbox;
    if (pt.x < x[0] || pt.x > x[1] ||
        pt.y < x[0] || pt.y > y[1]) {
      return false;
    }
  }
  // ...
}

//별칭을 사용하는 것보다 구조 분해 할당으로 변수를 뽑아내는 것이 더 좋음.

async/await 사용하기

function fetchURL(url: string, cb: (response: string) => void) {
  cb(url);
}
const url1 = '1';
const url2 = '2';
const url3 = '3';
// END
fetchURL(url1, function(response1) {
  fetchURL(url2, function(response2) {
    fetchURL(url3, function(response3) {
      // ...
      console.log(1);
    });
    console.log(2);
  });
  console.log(3);
});
console.log(4);

// Logs:
// 4
// 3
// 2
// 1
async function fetchPages() {
  const response1 = await fetch(url1);
  const response2 = await fetch(url2);
  const response3 = await fetch(url3);
  // ...
}

//콜백보다는 프로미스나 async/await이 코드를 작성하기 쉽고 코드를 추론하기 쉽다.
async function fetchPages() {
  const [response1, response2, response3] = await Promise.all([
    fetch(url1), fetch(url2), fetch(url3)
  ]);
  // ...
}

//여러 비동기처리를 병렬로 하고 싶을 때는 Promise.all, 구조분해할당, await을 사용하면 response1,2,3을 Response로 추론함
function fetchPagesCB() {
  let numDone = 0;
  const responses: string[] = [];
  const done = () => {
    const [response1, response2, response3] = responses;
    // ...
  };
  const urls = [url1, url2, url3];
  urls.forEach((url, i) => {
    fetchURL(url, r => {
      responses[i] = url;
      numDone++;
      if (numDone === urls.length) done();
    });
  });
}

//콜백을 사용하여 여러 비동기요청을 병렬처리하면 추론해야하는 타입이 많아짐.
function timeout(millis: number): Promise<never> {
  return new Promise((resolve, reject) => {
     setTimeout(() => reject('timeout'), millis);
  });
}

async function fetchWithTimeout(url: string, ms: number) {
  return Promise.race([fetch(url), timeout(ms)]);
}

//Promise.race를 사용하면 타입추론이 작동함
const _cache: {[url: string]: string} = {};
function fetchWithCache(url: string, callback: (text: string) => void) {
  if (url in _cache) {
    callback(_cache[url]);
  } else {
    fetchURL(url, text => {
      _cache[url] = text;
      callback(text);
    });
  }
}

//함수는 반드시 비동기/동기 중 한가지 방식으로만 작동해야 한다. 캐시된 경우 콜백함수가 동기로 호출되기 때문에 사용하기 어려워짐
// Function getJSON(url: string): Promise<any>
async function getJSON(url: string) {
  const response = await fetch(url);
  const jsonPromise = response.json();  // Type is Promise<any>
  return jsonPromise;
}

//이미 promise인 경우 Promise<Promise<any>>가 아니라 Promise<any>로 인식함

타입 추론 작동 이해하기

type Language = 'JavaScript' | 'TypeScript' | 'Python';
function setLanguage(language: Language) { /* ... */ }

setLanguage('JavaScript');  // OK

let language = 'JavaScript';
setLanguage(language);
         // ~~~~~~~~ Argument of type 'string' is not assignable
         //          to parameter of type 'Language'

//language를 선언한 시점에 타입이 string로 추론되어서 에러 발생
type Language = 'JavaScript' | 'TypeScript' | 'Python';
function setLanguage(language: Language) { /* ... */ }
let language: Language = 'JavaScript';
setLanguage(language);  // OK

//language 선언 때 타입을 지정해주는 방법을 해결
const language = 'JavaScript';
setLanguage(language);  // OK

//const를 사용하여 타입 제한
function panTo(where: [number, number]) { /* ... */ }
const loc = [10, 20] as const;
panTo(loc);
   // ~~~ Type 'readonly [10, 20]' is 'readonly'
   //     and cannot be assigned to the mutable type '[number, number]'

//as const는 타입을 너무 과하게 추론하여 타입체크 미통과
function panTo(where: readonly [number, number]) { /* ... */ }
const loc = [10, 20] as const;
panTo(loc);  // OK

//readonly를 사용하여 as const로 정의한 타입의 타입체크 통과
function callWithRandomNumbers(fn: (n1: number, n2: number) => void) {
  fn(Math.random(), Math.random());
}
const fn = (a, b) => {
         // ~    Parameter 'a' implicitly has an 'any' type
         //    ~ Parameter 'b' implicitly has an 'any' type
  console.log(a + b);
}
callWithRandomNumbers(fn);

//콜백함수를 따로 선언할 경우 매개변수가 any로 추론됨


4장 타입 설계

유효한 상태만 표현하는 타입을 지향하기

interface State {
  pageText: string;
  isLoading: boolean;
  error?: string;
}
//웹페이지 상태를 가정

declare let currentPage: string;
function renderPage(state: State) {
  if (state.error) {
    return `Error! Unable to load ${currentPage}: ${state.error}`;
  } else if (state.isLoading) {
    return `Loading ${currentPage}...`;
  }
  return `<h1>${currentPage}</h1>\n${state.pageText}`;
}

//error가 선택적 필드이기 때문에 발생한지 안한지 알 수 없음, 분기 부정확
async function changePage(state: State, newPage: string) {
  state.isLoading = true;
  try {
    const response = await fetch(getUrlForPage(newPage));
    if (!response.ok) {
      throw new Error(`Unable to load ${newPage}: ${response.statusText}`);
    }
    const text = await response.text();
    state.isLoading = false;
    state.pageText = text;
  } catch (e) {
    state.error = '' + e;
  }
}

//에러 발생시 로딩 상태가 false가 되는 로직없음
//error를 초기화하지 않아 이전 에러가 출력될 수 있음
interface RequestPending {
  state: 'pending';
}
interface RequestError {
  state: 'error';
  error: string;
}
interface RequestSuccess {
  state: 'ok';
  pageText: string;
}
type RequestState = RequestPending | RequestError | RequestSuccess;

interface State {
  currentPage: string;
  requests: {[page: string]: RequestState};
}

//유니온 타입을 사용하여 웹페이지의 상태를 명시적으로 분리

사용할 때는 너그럽게, 생성할 때는 엄격하게

interface CameraOptions {
  center?: LngLat;
  zoom?: number;
  bearing?: number;
  pitch?: number;
}
type LngLat =
  { lng: number; lat: number; } |
  { lon: number; lat: number; } |
  [number, number];
type LngLatBounds =
  {northeast: LngLat, southwest: LngLat} |
  [LngLat, LngLat] |
  [number, number, number, number];
declare function setCamera(camera: CameraOptions): void;
declare function viewportForBounds(bounds: LngLatBounds): CameraOptions;

//LngLat과 LngLatBounds의 타입이 자유로워서 경우의 수가 많음
interface LngLat { lng: number; lat: number; };
type LngLatLike = LngLat | { lon: number; lat: number; } | [number, number];
interface CameraOptions {
  center?: LngLatLike;
  zoom?: number;
  bearing?: number;
  pitch?: number;
}

// interface Camera {
//   center: LngLat;
//   zoom: number;
//   bearing: number;
//   pitch: number;
// }
// interface CameraOptions extends Omit<Partial<Camera>, 'center'> {
//   center?: LngLatLike;
// }
//를 명시적을 표현
type Feature = any;
declare function calculateBoundingBox(f: Feature): [number, number, number, number];
interface LngLat { lng: number; lat: number; };
type LngLatLike = LngLat | { lon: number; lat: number; } | [number, number];

interface Camera {
  center: LngLat;
  zoom: number;
  bearing: number;
  pitch: number;
}
interface CameraOptions extends Omit<Partial<Camera>, 'center'> {
  center?: LngLatLike;
}
type LngLatBounds =
  {northeast: LngLatLike, southwest: LngLatLike} |
  [LngLatLike, LngLatLike] |
  [number, number, number, number];

declare function setCamera(camera: CameraOptions): void;
declare function viewportForBounds(bounds: LngLatBounds): Camera;
function focusOnFeature(f: Feature) {
  const bounds = calculateBoundingBox(f);
  const camera = viewportForBounds(bounds);
  setCamera(camera);
  const {center: {lat, lng}, zoom} = camera;  // OK
  zoom;  // Type is number
  window.location.search = `?v=@${lat},${lng}z${zoom}`;
}

//매개변수의 타입이 반환타입보다 느슨한 경향이 있음

문서에 타입 정보를 쓰지 않기

/**
 * Returns a string with the foreground color.
 * Takes zero or one arguments. With no arguments, returns the
 * standard foreground color. With one argument, returns the foreground color
 * for a particular page.
 */
function getForegroundColor(page?: string) {
  return page === 'login' ? {r: 127, g: 127, b: 127} : {r: 0, g: 0, b: 0};
}

//주석과 코드가 일치하지 않고 코드만 보아도 명확하게 알 수 있는 정보가 포함되어 있음
type Color = { r: number; g: number; b: number };
// END
/** Get the foreground color for the application or a specific page. */
function getForegroundColor(page?: string): Color {
  // COMPRESS
  return page === 'login' ? {r: 127, g: 127, b: 127} : {r: 0, g: 0, b: 0};
  // END
}

//타입스크립트는 주석이 없이도 코드를 해석하기 용이하기 때문에 코드 자체가 주석의 역할을 함
function sort(nums: readonly number[]) { /* ... */ }

//변수를 변경하지 말라고 주석을 다는 것보다 readonly를 사용하여 규칙을 강제로 지키게 함
//ageNum을 사용하는 것보다 age:number를 사용하여 규칙을 강제하고 주석 역할을 함. 단, 단위가 있는 경우는 명시

타입 주변에 null 값 배치하기

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);
                  // ~~~ Argument of type 'number | undefined' is not
                  //     assignable to parameter of type 'number'
    }
  }
  return [min, max];
}

//0이 포함되어 있는 경우나, nums 배열이 비어있는 경우 의도치 않은 값 리턴
function extent(nums: number[]) {
  let result: [number, number] | null = null;
  for (const num of nums) {
    if (!result) {
      result = [num, num];
    } else {
      result = [Math.min(num, result[0]), Math.max(num, result[1])];
    }
  }
  return result;
}

//null이거나 아니거나로 나누어서 에러 해결
interface UserInfo { name: string }
interface Post { post: string }
declare function fetchUser(userId: string): Promise<UserInfo>;
declare function fetchPostsForUser(userId: string): Promise<Post[]>;
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 fetchPostsForUser(userId)
    ]);
  }

  getUserName() {
    // ...?
  }
}

//네트워크 요청이 로드되는 동안 user와 posts 속성은 null 상태여서 4가지의 경우의 수가 생김

유니온의 인터페이스보다는 인터페이스의 유니온을 사용하기

type FillPaint = unknown;
type LinePaint = unknown;
type PointPaint = unknown;
type FillLayout = unknown;
type LineLayout = unknown;
type PointLayout = unknown;
interface Layer {
  layout: FillLayout | LineLayout | PointLayout;
  paint: FillPaint | LinePaint | PointPaint;
}
//인터페이스 내의 유니온타입보다는

type FillPaint = unknown;
type LinePaint = unknown;
type PointPaint = unknown;
type FillLayout = unknown;
type LineLayout = unknown;
type PointLayout = unknown;
interface FillLayer {
  layout: FillLayout;
  paint: FillPaint;
}
interface LineLayer {
  layout: LineLayout;
  paint: LinePaint;
}
interface PointLayer {
  layout: PointLayout;
  paint: PointPaint;
}
type Layer = FillLayer | LineLayer | PointLayer;

//인터페이스 정의 후 유니온 타입을 정의
type FillPaint = unknown;
type LinePaint = unknown;
type PointPaint = unknown;
type FillLayout = unknown;
type LineLayout = unknown;
type PointLayout = unknown;
interface FillLayer {
  type: 'fill';
  layout: FillLayout;
  paint: FillPaint;
}
interface LineLayer {
  type: 'line';
  layout: LineLayout;
  paint: LinePaint;
}
interface PointLayer {
  type: 'paint';
  layout: PointLayout;
  paint: PointPaint;
}
type Layer = FillLayer | LineLayer | PointLayer;

//인터페이스의 유니온을 사용하는 태그된 유니온 사용의 예

string 타입보다 더 구체적인 타입 사용하기

interface Album {
  artist: string;
  title: string;
  releaseDate: string;  // YYYY-MM-DD
  recordingType: string;  // E.g., "live" or "studio"
}
//구체적이지 않은 타입 설정

type RecordingType = 'studio' | 'live';

interface Album {
  artist: string;
  title: string;
  releaseDate: Date;
  recordingType: RecordingType;
}
//구체적으로 타입을 설정하기
type RecordingType = 'studio' | 'live';

interface Album {
  artist: string;
  title: string;
  releaseDate: Date;
  recordingType: RecordingType;
}
function pluck<T>(record: T[], key: keyof T) {
  return record.map(r => r[key]);
}

//key of를 사용하여 T 객체 내의 값의 타입으로 지정
function pluck<T>(record: T[], key: keyof T) {
  return record.map(r => r[key]);
}
declare let albums: Album[];
const releaseDates = pluck(albums, 'releaseDate'); // Type is (string | Date)[]

//keyof 를 사용하여더라도 releaseDates의 타입이 Date[]가 아닌 (string | Date)[]로 여전히 범위가 넓음
function pluck<T, K extends keyof T>(record: T[], key: K): T[K][] {
  return record.map(r => r[key]);
}

//T[K]로 출력 값의 타입을 좁힘

부정확한 타입보다는 미완성 타입을 사용하기

type GeoPosition = [number, number];
interface Point {
  type: 'Point';
  coordinates: GeoPosition;
}

//위도와 경도를 나타내는 Point는 튜플타입을 사용했으나 고도와 같은 값이 추가될 경우 타입을 무시해야함
type Expression1 = any;
type Expression2 = number | string | any[];
type Expression4 = number | string | CallExpression;

type CallExpression = MathCall | CaseCall | RGBCall;

interface MathCall {
  0: '+' | '-' | '/' | '*' | '>' | '<';
  1: Expression4;
  2: Expression4;
  length: 3;
}

interface CaseCall {
  0: 'case';
  1: Expression4;
  2: Expression4;
  3: Expression4;
  length: 4 | 6 | 8 | 10 | 12 | 14 | 16 // etc.
}

interface RGBCall {
  0: 'rgb';
  1: Expression4;
  2: Expression4;
  3: Expression4;
  length: 4;
}

const tests: Expression4[] = [
  10,
  "red",
  true,
// ~~~ Type 'true' is not assignable to type 'Expression4'
  ["+", 10, 5],
  ["case", [">", 20, 10], "red", "blue", "green"],
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//  Type '["case", [">", ...], ...]' is not assignable to type 'string'
  ["**", 2, 31],
// ~~~~~~~~~~~~ Type '["**", number, number]' is not assignable to type 'string
  ["rgb", 255, 128, 64],
  ["rgb", 255, 128, 64, 73]
// ~~~~~~~~~~~~~~~~~~~~~~~~ Type '["rgb", number, number, number, number]'
//                          is not assignable to type 'string'
];
 const okExpressions: Expression4[] = [
   ['-', 12],
// ~~~~~~~~~ Type '["-", number]' is not assignable to type 'string'
   ['+', 1, 2, 3],
// ~~~~~~~~~~~~~~ Type '["+", number, ...]' is not assignable to type 'string'
   ['*', 2, 3, 4],
// ~~~~~~~~~~~~~~ Type '["*", number, ...]' is not assignable to type 'string'
 ];

//인터페이스를 사용하여 케이스를 나누고 튜플로 정의하여 모든 에러를 찾아냄
//모든 에러를 찾아내지만 에러 메세지가 부정확해져서 자동완성을 방해함
//위와 같은 경우에는 느슨한 타입을 사용하는 것이 더 효율적임

데이터가 아닌, API와 명세를 보고 타입 만들기

export interface getLicense_repository_licenseInfo {
  __typename: "License";
  /** Short identifier specified by <https://spdx.org/licenses> */
  spdxId: string | null;
  /** The license full name specified by <https://spdx.org/licenses> */
  name: string;
}

export interface getLicense_repository {
  __typename: "Repository";
  /** The description of the repository. */
  description: string | null;
  /** The license associated with the repository */
  licenseInfo: getLicense_repository_licenseInfo | null;
}

export interface getLicense {
  /** Lookup a given repository by the owner and repository name. */
  repository: getLicense_repository | null;
}

export interface getLicenseVariables {
  owner: string;
  name: string;
}

//+추가 타입스크립트와 비슷한 타입 시스템을 가지는 GraphQL
//API의 타입을 직접 생성하지 말고 명세로 부터 타입을 생성
//Apollo: GraphQl 타입을 타입스크립트 타입으로 변경해주는 도구
//quicktype 데이터로부터 타입을 생성하는 도구

해당 분야의 용어로 타입 이름 짓기

interface Animal {
  name: string;
  endangered: boolean;
  habitat: string;
}

const leopard: Animal = {
  name: 'Snow Leopard',
  endangered: false,
  habitat: 'tundra',
};

//타입의 이름이 모호해서 의미가 불분명함, 나쁜 예
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' |
  'BSh' | 'BSk' | 'BWh' | 'BWk' |
  'Cfa' | 'Cfb' | 'Cfc' | 'Csa' | 'Csb' | 'Csc' | 'Cwa' | 'Cwb' | 'Cwc' |
  'Dfa' | 'Dfb' | 'Dfc' | 'Dfd' |
  'Dsa' | 'Dsb' | 'Dsc' | 'Dwa' | 'Dwb' | 'Dwc' | 'Dwd' |
  'EF' | 'ET';
const snowLeopard: Animal = {
  commonName: 'Snow Leopard',
  genus: 'Panthera',
  species: 'Uncia',
  status: 'VU',  // vulnerable
  climates: ['ET', 'EF', 'Dfd'],  // alpine or subalpine
};


//타입의 기준과 이름, 제한된 유니온 타입 등이 분명함, 좋은 예
//해당 분야의 이름 사용, 같은 의미에는 같은 이름 사용

공식 명칭에는 상표를 붙이기

interface Vector2D {
  _brand: '2d';
  x: number;
  y: number;
}
function vec2D(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);  // Same as before
}

calculateNorm(vec2D(3, 4)); // OK, returns 5
const vec3D = {x: 3, y: 4, z: 1};
calculateNorm(vec3D);
           // ~~~~~ Property '_brand' is missing in type...
//Vector2D 타입만 받을 수 있도록 vec2D 함수를 통해 객체 생성
type SortedList<T> = T[] & {_brand: 'sorted'};

function isSorted<T>(xs: T[]): xs is SortedList<T> {
  for (let i = 1; i < xs.length; i++) {
    if (xs[i] > xs[i - 1]) {
      return false;
    }
  }
  return true;
}

function binarySearch<T>(xs: SortedList<T>, x: T): boolean {
  // COMPRESS
  return true;
  // END
}

//상표 기법과 사용자 정의 타입 가드를 사용하여 정렬되어 있는 배열인지 검사
type Meters = number & {_brand: 'meters'};
type Seconds = number & {_brand: 'seconds'};

const meters = (m: number) => m as Meters;
const seconds = (s: number) => s as Seconds;

const oneKm = meters(1000);  // Type is Meters
const oneMin = seconds(60);  // Type is Seconds
const tenKm = oneKm * 10;  // Type is number
const v = oneKm / oneMin;  // Type is number

//number도 상표 기법을 사용할 수 있지만 연산 후 사라짐


마치며

책을 읽고 실무에서 작성된 코드를 보면서 타입스크립트는 사용자가 어떻게 사용하냐에 따라 장점을 끌어올릴 수 있는 언어라는 생각이 들었다. 다른 언어와 다르게 tsconfig.json으로 설정할 수 있는 옵션이 다양하고, 타입스크립트를 사용한다고 하더라도 잘못 사용하면 자바스크립트와 다르지 않을 수도 있다. 결국 사용자가 타입스크립트에 대해 잘 이해하고 상황에 맞게 제대로 사용해야 의미가 있는 언어라는 생각이 들었다.

좋은 웹페이지 즐겨찾기