아이템 41 any 진화를 이해하기

요약

  • 일반적인 타입들은 정제되기만 하는 반면 (ex. 타입 좁히기), 암시적 any와 any[] 타입은 진화할 수 있습니다.
    이러한 동작이 발생하는 코드를 인지하고 이해할 수 있어야합니다.
  • any를 진화시키는 방식보다 명시적 타입 구문을 사용하는 것이 안전한 타입을 유지하는 방법 입니다.

 
1. 일반적인 타입들은 정제되기만 하는 반면, 암시적 any와 any[] 타입은 진화할  있습니다.

  • ❗️타입의 진화는 값을 할당하거나 배열의 요소를 넣은 ‘후’에만 일어나기 때문에, 편집기에서는 할당 다음 줄을 봐야 진화된 타입이 잡힌다.
//1. 배열의 any 타입 진화(evolve)
const result = [];  //any[]
result.push('a');  
result   //string[]
result.push(1);
result   //(string | number)[]
//2. 조건문에 따른 any 타입 진화(evolve)
let val;  //any
if(Math.random() < 0.5) {
    val = /hello/;
    val  //RegExp
} else {
    val = 12;
    val  //number
}

val  //number | RegExp
//3. 초깃값이 null 인 경우 any 타입 진화(evolve)
let tmp = null;  //any
try {
    somethingDangerous();
    tmp = 12;
    tmp  //number
} catch (e) {
    console.log('err!');
}

tmp  //number | null
  • any 타입의 진화는 "noImplicitAny": true 로 설정된 상태에서 변수의 타입이 암시적 any인 경우에만 일어난다. → 명시적인 경우 진화가 일어나지 않는다.
//명시적인 경우 진화가 일어나지 않는다.
let val2: any;  //any
if(Math.random() < 0.5) {
    val2 = /hello/;
    val2  //any
} else {
    val2 = 12;
    val2  //any
}

val2  //any

 
2. any를 진화시키는 방식보다 명시적 타입 구문을 사용하는 것이 안전한 타입을 유지하는 방법이다.

  • 암시적 any 상태인 변수에 어떠한 할당 없이 사용하려 하면 암시적 any 오류가 발생한다.
function range(start: number, limit: number) {
    const out = []; //암시적 any
    if (start === limit) {
        return out; //❌ Error:'out' 변수에는 암시적으로 'any[]' 형식이 포함됩니다.
    }

    for(let i = start; i < limit; i++) {
        out.push(i);
    }
    return out;  // number[] 로 추론되기 때문에 에러 없음.
}
  • 암시적 any 타입은 함수 호출을 거쳐도 진화하지 않는다.
function makeSqure(start: number, limit: number) {
    const out = []; //❌ ''out' 변수는 형식을 확인할 수 없는 경우 일부 위치에서 암시적으로 'any[]' 형식입니다.
    range(start, limit).forEach(i => {
        out.push(i * i);
    })
    return out; 
}
  • 의도치 않은 타입이 섞여서 잘못 진화할 수 있기 때문에, (암시적 any 진화 방식보단) 명시적 타입 구문 사용이 더 좋은 설계다.
const result = [];  //any[]
result.push(1);
result   //number[]

//보다는 처음부터 명시
const result: number[] = [];

 

아이템 42 모르는 타입의 값에는 any 대신 unknown사용하기

요약

  • unknown은 any 대신 사용할 수 있는 안전한 타입이다. 어떠한 값이 있지만 그 타입을 알지 못하는 경우라면 unknown을 사용함으로써 타입 단언문이나 타입 체크를 사용하도록 강제할 수 있다. (any 일 때 타입 체크가 안 되는 문제점이 보완된다.)
  • unknown는 모든 타입의 상위 타입이고, never는 모든 타입의 하위 타입이다. unknown 타입은 모든 타입이 될 수 있으나 모든 타입은 unknown이 될 수 없다. 반대로 never 타입은 모든 타입이 될 수 없고 모든 타입은 never가 될 수 있다.
  • {} 타입은 null과 undefined를 제외한 모든 값을 포함한다.

1. 어떠한 값이 있지만 그 타입을 알지 못하는 경우라면 any 대신 unknown을 사용하자.

  • unknown을 사용함으로써 타입 단언문이나 타입 체크를 사용하도록 강제할 수 있다.
    (any일 때 타입 체크가 안 되는 문제점이 보완된다.)
  • unknown 은 unknown 인 채로 사용하면 오류가 발생한다.
interface Book {
    name: string,
    author: string
}

//❌ 반환값이 any 여서 타입 체크가 안되어 런타임 에러가 발생한다.
const parseYamL = (yaml: string): any => {};

const book = parseYamL(`name: Jane 
                        author: Char`);
                        
//parseYamL를 호출한 곳에서 타입 선언이나 타입 단언을 하지 않으면 book이 any가 된다.
//따라서 아래 코드는 타입 에러도 나지 않고 런타임에서 에러가 난다.
console.log(book.title);
book();

//✅ 대신 unknown을 쓰자.
const safetyParseYamL = (text: string): unknown => ({});

const book2 = safetyParseYamL(`name: Jane 
                                author: Char`);
//unknown은 타입에러가 난다! 런타임 단계가 아니라 컴파일 단계에서 에러 확인 가능하다.

console.log(book2.title); // 'book2'은(는) 'unknown' 형식입니다.
book2(); // 'book2'은(는) 'unknown' 형식입니다.
  • 이중 단언문에서도 any 대신 unknown 을 사용할 수도 있다.
declare const foo: Foo;

let barAny = foo as any as Bar;
let barUnk = foo as unknown as Bar;

 
2. unknown는 모든 타입의 상위 타입이고, never는 모든 타입의 하위 타입이다.

  • 타입의 값을 집합으로 생각하기(아이템7)를 참고해보자.
  • any 타입은 모든 타입에 할당 될 수도 있고, 모든 타입이 any에 할당 될 수 있으니, 타입 시스템의 열외로 본다. 하지만 unknown은 타입 시스템에 부합한다.
  • any를 제외한 일반적인 타입은 타입 좁히기(더 작은 집합 되기)만 가능하다. 따라서 unknown 타입은 모든 타입이 될 수 있으나 모든 타입은 unknown이 될 수 없다. 반대로 never 타입은 모든 타입이 될 수 없고 모든 타입은 never가 될 수 있다.
let num: number;

function somethingDo(): unknown {
    return;
}

num = somethingDo(); //❌ 'unknown' 형식은 'number' 형식에 할당할 수 없습니다.

function hello(): never {
    throw new Error("xxx");
}

num = hello();  //✅

let imNever =  hello();
imNever = 12;  //❌ 'number' 형식은 'never' 형식에 할당할 수 없습니다

 
3. {} 타입은 null과 undefined를 제외한 모든 값을 포함한다.

  • unknown 타입이 도입되기 이전에는 {}가 일반적으로 사용되었다. 최근에는 잘 사용하지 않는다.
  • 정말로 null과 undefined가 불가능하다고 판단되는 경우에만 {}를 대신 사용해야 한다.
  • object 타입은 모든 비기본형(non-primitive) 타입으로 이루어진다. ex) 객체, 배열

 

아이템 43 몽키 패치보다는 안전한 타입을 사용하기

요약

  • 몽키패치란, 원래 소스코드를 변경하지 않고 실행 시 코드 기본 동작을 추가, 변경 또는 억제하는 기술을 의미한다. 자바스크립트에 있어서는 프로토타입에 특정 메소드 추가 한다거나 document 객체에 전역 변수를 삽입하는 등이 있다.
  • 전역 변수나 DOM에 데이터를 저장하는 것보다 데이터를 분리하여 사용해야 한다.
  • 어쩔수 없이 내장 타입에 데이터를 저장해야 하는 경우, 안전한 타입 접근법 중 하나인 보강이나 사용자 정의 인터페이스로 단언를 사용해야 한다
//보강
interface Document {
    monkey: string;
}

document.monkey = 'Tamarin';   //정상

//사용자 정의 인터페이스로 단언
interface MonkeyDocument extends Document {
    monkey: string;
}

(document as MonkeyDocument).monkey = 'Tamarin';  //정상

 

아이템 44 타입 커버리지를 추적하여 타입 안전성 유지하기

요약

  • noImplicitAny로 암묵적인 any타입 사용을 금지 시켜도, 명식적 any 또는 서드파티 타입 선언(@types)을 통해 any 타입은 코드 내에 여전히 존재할 수 있다는 점을 주의하자.
  • 작성한 프로그램 타입을 추적해 any 사용을 줄여나가자. npx type-coverage 를 사용해 프로젝트 심벌중 any 가 아닌 타입의 퍼센트를 확인할 수 있다. npx type-coverage --detail 를 사용해 any 타입이 있는 곳을 모두 출력할 수 있다.
$ npx type-coverage
// 9985 / 10117 98.69%

 

6장 타입 선언과 @types


아이템 45 devDependencies 에 typescript와 @types 추가하기

 
요약

  • typescript를 시스템 레벨(npm install -g typescript)로 설치하면 안 된다.
    typescript를 프로젝트의 devDependencies 에 포함시키고 팀원 모두가 동일한 버전을 사용하도록 해야 한다.
  • @types 의존성은 dependencies가 아니라 devDependencies에 포함시켜야 한다.
    런타임에 @types 가 필요한 경우라면 별도의 작업이 필요할 수 있다.

예시

//package.json
{
  ...
  "dependencies": {
    "lodash": "^4.17.21"
  },
  "devDependencies": {
    "typescript": "^5.0.2",
    "@types/lodash": "^4.14.192",
    "@types/jest": "^29.5.0",
    "jest": "^29.5.0"
  }
}
      • dependencies
        • 현재 프로젝트를 실행하는 필수적인 라이브러리들이 포함
        • 프로젝트를 npm 공개하여 다른 사용자가 해당 프로젝트를 설치한다면, dependencies 들어 있는 라이브러리도 함께 설치될 것
    • devDependencies
      • 현재 프로젝트를 개발하고 테스트하는 데 사용되지만, 런타임에는 필요 없는 라이브러리들이 포함됨

모든 타입스크립트 프로젝트에서 공통적으로 고려해야 의존성 2가지
1. 타입스크립트 자체 의존성을 고려해야 한다.

  • 시스템 레벨로 설치할 수 있지만, 팀원들 모두 동일한 버전을 설치한다고 보장할 수 없고, 프로젝트를 셋업할 때 별도의 단계가 추가됨
  • 따라서 타입스크립트를 시스템 레벨로 설치하기보다는 devDependencies에 넣는 것이 좋다.
    devDependencies에 포함되어 있다면, npm install을 실행할 때 팀원들 모두 항상 정확한 버전의 타입스크립트를 설치할 수 있습니다.

2. 타입 의존성(@types)을 고려해야 한다.

DefinitelyTyped
타입스크립트 커뮤니티에서 유지보수하고 있는 자바스크립트 라이브러리의 타입을 정의한 모음

사용하려는 라이브러리에 타입 선언이 포함되어 있지 않더라도, DefinitelyTyped에서 타입을 얻을 수 있다.
-> 원본 라이브러리 자체가 dependencies에 있더라도 ©types 의존성은 dev Dependencies에 있어야 합니다.
 
예시. 리액트의 타입 선언과 리액트를 의존성에 추가하는 명령어

$ npm install react
$ npm install --save-dev @types/react

그러면 다음과 같은 package.json 파일이 생성됨

{
    "devDependencies": {
        "@types/react": "~L6.8.19",
        "typescript": "3.5.3"
    },
    "dependencies": {
    	"react": "16.8.6"
    }
}

 

아이템 46 타입 선언과 관련된 가지 버전 이해하기

요약

  • 타입 선언과 관련된 세가지 버전이 있다. 라이브러리 버전, @types 버전, typescript 버전이다. 이 세가지의 버전에 따라 타입스크립트 생태계에선 더욱 의존성이 관리가 복잡하게 되었다.
  • 라이브러리를 업데이트하는 경우, 해당 @types 역시 업데이트를 하자.
  • 라이브러리를 만들때 타입 선언을 관리하는 방법은 두가지가 있다.
    • 방법1) 타입 선언을 DefinitelyTyped에 공개(= @types)해 라이브러리와 따로 두기
    • 방법2) 타입 선언을 라이브러리에 포함시키기
    • 공식적인 권장사항은 타입스크립트로 작성된 라이브러리인 경우만 타입 선언을 자체적으로 포함하는 것이다. 그 외의 경우(자바스크립트로 작성된 라이브러리)라면 타입을 DefinitelyTyped에 공개해 따로 관리하는게 좋다.

실제 라이브러리와 타입 정보의 버전이 별도로 관리되는 방식의 문제점

// 특정 라이브러리 - dependencies로 설치
$ npm install react
+ react@16.8.6

// 타입 정보 - devDependencies
$ npm install —save-dev @types/react
+ @types/react@16.8.19

-> 메이저 버전과 마이너 버전(16.8) 일치하지만 패치 버전(.6 .19) 일치하지 않다.
 
1. 라이브러리를 업데이트했지만 실수로 타입 선언은 업데이트하지 않는 경우
라이브러리 업데이트와 관련된 새로운 기능을 사용하려 할 때마다 타입 오류가 발생하게 됨
 
2. 라이브러리보다 타입 선언의 버전이 최신인 경우
타입 정보 없이 라이브러리를 사용해 오다가 타입 선언을 설치하려고 할 때 발생하게 됨
-> 타입 체커는 최신 API를 기준으로 코드를 검사하게 되지만 런타임에 실제로 쓰이는 것은 과거 버전
 
3. 프로젝트에서 사용하는 타입스크립트 버전보다 라이브러 리에서 필요로 하는 타입스크립트 버전이 최신인 경우
-> 프로젝트의 타입스크립트 버전을 올리거나 or
라이브러리 타입 선언의 버전을 원래대로 내리거나 or 
declare module 선언으로 라이브러리의 타입 정보를 없애 버리기

4. @types 의존성이 중복될 수 있음

 

아이템 47 공개 API 등장하는 모든 타입을 익스포트하기

요약

  • 공개 메서드에 등장한 어떤 형태의 타입이든 익스포트 하자.
    어차피 라이브러리 사용자가 추출할 수 있으므로 익스포트 하기 쉽게 만드는 것이 좋다. (어차피 숨기는게 거의 불가능하다.)

예시

interface SecretName {
    first: string;
    last: string;
}
interface SecretSanta {
    name: SecretName;
    gift: string;
}

export const getGift = (name: SecretName, gift: string): SecretSanta => {
    //...
};

// 타입 추출하기
type MySanta = ReturnType<typeof getGift>;     //SecretSanta
type MyName = Parameters<typeof getGift>[0];   //SecretName

 
 
 
출처: https://www.dgmunit1.com/blog/typescript/item45_52

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

타입이 구체적일수록 버그를 더 많이 잡고 타입스크립트가 제공하는 도구를 활용할 수 있게 된다.

하지만 부정확한 타입 설계는 없는 것보다 못하다

그리고 타입정보를 구체적으로 만들 수록 오류 메시지와 자동 완성 기능에 주의를 기울여야 한다.

 

예시 1. 타입을 구체적으로 개선한 코드

interface Point {
    type: 'Point';
    coordinates: number[];
}
interface Linestring {
    type: 'LineString';
    coordinates: number[][];
}
interface Polygon {
    type: 'Polygon';
    coordinates: number[][][];
}
type Geometry = Point | LineString | Polygon;

 

-> 좌표로 쓰이는 number[]가 추상적

여기서 number[] 경도와 위도를 나타내므로 튜플 타입으로 선언하는게 낫다.

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

 

타입을 구체적으로 개선했기 때문에 더 나은 코드가 됨

 

예시 2. JSON으로 정의된 Lisp와 비슷한 언어의 타입 선언

Lisp
리스프(Lisp, LISP) 혹은 리습은 프로그래밍 언어의 하나로, 오랜 역사와 독특하게 괄호를 사용하는 문법으로 유명하다.
Lisp은 “LISt Processing”(리스트 프로세싱)의 줄임말.
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'
 ];

CaseCall 타입이 사용된 요소에서는 배열의 길이가 초과하여 오류가 발생하였는데,
오류로 표현되어야 할
green이 아닌 MathCall 타입의 [”>”, 20, 10] 요소가 오류로 지목되었다.

 

Good

코드를 더 정밀하게 만들려던 시도가 너무 과했고 그로 인해 코드가 오히려 더 부정확해졌다.

이렇게 부정확함을 바로잡는 방법을 쓰는 대신, 테스트 세트를 추가하여 놓친 부분이 없는지 확인하는 것이 나은 방법일 수 있다.


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

여기서 핵심은 예시 데이터가 아니라 명세를 참고해 타입을 생성한다는 것

예시 데이터를 참고해 타입을 생성하면 눈앞에 있는 데이터들만 고려하게 되므로 예기치 않은 곳에서 오류가 발생할 수 있으나

명세를 이용해 타입을 생성하면 타입스크립트는 사용자가 실수를 줄일 수 있게 도와준다.

 

예시. GitHub 저장소에서 오픈 소스 라이선스를 조회하는 쿼리

GraphQL API는 타입스크립트와 비슷한 타입 시스템을 사용하여, 가능한 모든 쿼리와 인터페이스를 명세하는 스키마로 이루어진다.

우리는 이러한 인터페이스를 사용해서 특정 필드를 요청하는 쿼리를 작성한다.

 

GraphQL의 장점은 특정 쿼리에 대해 타입스크립트 타입을 생성할 수 있다는 것이다.

GeoJSON 예제와 마찬가지로 GraphQL을 사용한 방법도 타입에 null이 가능한지 여부를 정확하게 모델링할 수 있다.

query getLicense($owner: String!, $name:String!) {
  repository(owner:$owner, name:$name) {
    description
    licenseInfo {
      spdxId
      name
    }
  }
}

 

$owner와 $name은 타입이 정의된 GraphQL의 변수이다.

String은 GraphQL의 타입으로 타입스크립트의 string에 대응

타입 뒤의 ’!’는 null이 아님을 명시

 

Apollo는 GraphQL 쿼리를 타입스크립트 타입으로 변환해 주는 도구 중 하나

쿼리에서 타입을 생성하려면 GraphQL 스키마가 필요하다.

 

Apollo는 api.github.com/graphql로 부터 스키마를 얻는다.

$ apollo client:codegen \
    —endpoint https: //api. git hub. com/graphql \
    —includes license.graphql \
    --target typescript

 

실행결과

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;
}

 

주목할 만한 점은 다음과 같다.

  • 쿼리 매개변수(getLicenseVariables)와 응답(getLicense) 모두 인터페이스가 생성되었다.
  • null 가능 여부는 스키마로부터 응답 인터페이스로 변환되었다.
  • 편집기에 확인할 수 있도록 주석은 JSDoc으로 변환되었다(아이템 48), 이 주석들은 GraphQL 스키마로부터 생성되었다.

=> 자동으로 생성된 타입 정보는 API를 정확히 사용할 수 있도록 도와준다.

쿼리가 바뀌면 타입도 자동으로 바뀌며, 스키마가 바뀌면 타입도 자동으로 바뀝니다.


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

 

엄선된 타입, 속성, 변수의 이름은 의도를 명학히 하고 코드와 타입의 추상화 수준을 높여 준다.

잘못 선택한 타입 이름은 코드의 의도를 왜곡하고 잘못된 개념을 심어주게 된다.

 

코드로 표현하고자 하는 모든 분야에는 주제를 설명하기 위한 전문 용어들이 있다.

이런 용어들을 사용하면 소통에 유리하고 타입의 명확성을 올릴 수 있다.

 

주의할 점 3가지

  • 동일한 의미를 나타낼 때는 같은 용어를 사용한다.
  • data, info, thing, item, object, entity 같은 모호하고 의미없는 이름은 피한다.

만약 entity라는 용어가 해당 분야에서 특별한 의미를 가진다면 괜찮다.

  • 이름을 지을 때는 포함된 내용이나 계산 방식이 아니라 데이터 자체가 무엇인지를 고려해야 한다.
예를 들어, INodeList 보다는 Directory가 더 의미 있는 이름.
Directory는 구현의 측면이 아닌 개념적인 측면에서 보는 것

 

 

예시. 동물들의 데이터베이스를 구축한다고 가정해 보자

Bad

  • name: 매우 일반적인 용어, 동물의 학명인지 일반적인 명칭인지 알 수 없음
  • endangered: 멸종 위기를 표현하기 위해 boolean 타입을 사용한 것이 맞지 않음
  • habitat: 너무 넓은 범위의 string 타입이며, 서식지를 뜻하는 지도 불분명
interface Animal {
  name: string;
  endangered: boolean;
  habitat: string;
}

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

Good

  • name을 commonName, genus, species 등 더 구체적인 용어로 대체
  • endangered를 status로 바꾸고, 동물 보호 등급에 대한 표준 체계를 사용하였다.
  • habitat을 climates으로 바꾸고 쾨펜 기후 분류를 사용하였다.
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
};

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

타입스크립트는 구조적 타이핑을 사용하기 때문에, 값을 세밀하게 구분하지 못하는 경우가 있다.

값을 구분하기 위해 공식 명칭이 필요하다면 상표를 붙이는 것을 고려해야 한다.

 

상표 기법은 타입 시스템에서 동작하지만 런타임에서 상표를 검사하는 것과 동일한 효과를 얻을 수 있다.

타입 시스템이기 때문에 런타임 오버헤드를 없앨 수 있고 추가 속성을 붙일 수 없는 string이나 number 같은 내장 타입도 상표화 할 수 있다.

구조적 타이핑
: 실제 구조와 정의에 의해 결정되는 타입 시스템

 

예시 1. 벡터의 길이를 계산하는 함수

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});  // OK, result is 5

const vec3D = {x: 3, y: 4, z: 1};
calculateNorm(vec3D);  // OK! result is also 5

 

Good

calculateNorm 함수가 3차원 벡터를 허용하지 않게 하려면 공식 명칭(nominal-typing)을 사용하면 된다.

공식 명칭을 사용하는 것은, 타입이 아니라 값의 관점에서 Vector2D라고 말하는 것

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...

 

그러나 vec3D 값에 _brand: '2d'라고 추가하는 같은 악의적인 용을 막을 수는 없다.

다만 단순한 실수를 방지하기에는 충분하다.

 

예시 2. 절대 경로를 사용해 파일 시스템에 접근하는 함수

Good

string 타입이면서 _brand 속성을 가지는 객체를 만들 수는 없다.

타입 AbsolutePath는 온전히 타입 시스템의 영역

 

만약 path 값이 절대 경로와 상대 경로 둘 다 될 수 있다면, 타입을 정제해 주는 타입 가드를 사용해서 오류를 방지할 수 있다.

type AbsolutePath = string & {_brand: 'abs'};

function listAbsolutePath(path: AbsolutePath) {
  // ...
}

function isAbsolutePath(path: string): path is AbsolutePath {
  return path.startsWith('/');
}

function f(path: string) {
  if (isAbsolutePath(path)) {
    listAbsolutePath(path);
  }
  listAbsolutePath(path);
                // ~~~~ Argument of type 'string' is not assignable
                //      to parameter of type 'AbsolutePath'
}

 

예시 3. number 타입에 상표 붙이기

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); // 타입이 Meters
const oneMin = seconds (60); // 타입이 Seconds

 

하지만, number 타입에 상표를 붙여도 산술 연산 후에는 상표가 없어지기 때문에 실제로 사용하기에는 무리가 있다. 

그러나 숫자의 단위를 문서화하는 괜찮은 방법일 수 있다.

const tenKm = oneKm * 10; // 타입이 number
const v = oneKm / oneMin; // 타입이 number

 


5장 any 다루기

아이템 38 any 타입은 가능한 한 좁은 범위에서만 사용하기

요약

  • 의도치 않은 타입 안전성의 손실을 피하기 위해서 any의 사용 범위를 최소한으로 좁혀야 합니다.
  • 함수의 반환 타입이 any인 경우 타입 안정성이 나빠집니다. 따라서 any 타입을 반환하면 절대 안 됩니다.
  • 강제로 타입 오류를 제거하려면 any 대신 @ts-ignore 사용하는 것이 좋습니다.

예시 1. 함수 내에서 사용된 any

function f1() {
  const x: any = expressionReturningFoo(); // 이렇게 하지 맙시다.
  processBar(x);
}
function f2() {
  const x = expressionReturningFoo();
  processBar(x as any); // 이게 낫습니다.
}

f1 함수: x를 any 타입으로 지정
f2 함수: x가 사용되는 곳에 as any 단언문 사용

 

f2 함수가 권장되는 이유:

any 타입이 processBar 함수의 매개변수에서만 사용이 되기 때문에, 다른 코드에는 영향이 미치지 않음 !

반면에, f1 함수에서는 마지막까지 x가 any 타입이 유지된다.

 

f1 함수가 x를 반환한다면?

function f1() {
  const x: any = expressionReturningFoo();
  processBar(x);
return x;

function g() {
  const foo = f1(); // 타입이 any
   foo.fooMethod(); // 이 함수 호출은 체크되지 않습니다!
}

 

=> 함수 호출이 체크되지 않기 때문에 이렇게 사용하면 안됨

이렇게 any 타입인 x를 반환하는 함수를 사용하면 foo 타입까지 영향을 미치기 때문에, 프로젝트 전반에 전염병처럼 퍼지게 된다.

 

f2 함수 처럼 사용한다면 any 타입이 함수 바깥으로 영향을 미치지 않는다.

function f1() {
    const x = expressionReturningFoo();
    // @ts-ignore
    processBar(x);
    return x;
}

@ts-ignore 를 사용하여 오류를 무시할 수 있지만, 근본적인 원인을 해결한 것은 아니기 때문에 다른 곳에 더 큰 문제가 발생할 수 있음

 

예시 2. 객체와 관련한 any 사용법

const config: Config = {
    a: 1,
    b: 2,
    c: {
        key: value
        // ~
        'foo' 속성이 'Foo' 타입에 필요하지만 'Bar' 타입에는 없습니다.
    }
};

 

단순히 생각하면 config 객체 전체를 as any로 선언해서 오류를 제거할 수 있다.

const config: Config = {
  a: 1,
  b: 2,
  c: {
  key: value
  }
} as any; // 이렇게 하지 맙시다!

 

하지만, 객체 전체를 any로 단언하면 다른 속성들 (a, b) 역시 타입 체크가 되지 않는다 !
그러므로 아래처럼 최소한의 범위에만 any를 사용하는 것이 좋다.

const config: Config = {
  a: 1,
  b: 2, // 이 속성은 여전히 체크됩니다.
  c: {
  key: value as any
  }
};

아이템 39 any를 구체적으로 변형해서 사용하기

요약

  • any를 사용할 때는 정말로 모든 값이 허용되어야만 하는지 면밀히 검토해야 한다.
  • any보다 더 정확하게 모델링 할 수 있도록 any[] 또는 {[id: string]: any} 또는 () => any처럼 구체적인 형태를 사용해야한다.

1. any를 사용할 때는 정말로 모든 값이 허용되어야만 하는지 면밀히 검토해야 한다.

  • any 타입은 모든 숫자, 문자열, 배열, 객체, 정규식, 함수, 클래스, DOM 엘레먼트, null, undefined 까지 포함한다.
  • 일반적인 상황에서 any 보다 더 구체적으로 표현할 수 있는 타입이 존재할 가능성이 높다.
const numArgsBad = (...args: any) => args.length; //❌ return 타입이 any
const numArgsGood = (...args: any[]) => args.length; //✅ return 타입이 number

/**
후자가 더 좋은 이유
1. args.length 타입이 체크 됨
2. 함수 반환 타입이 number 로 추론 됨
3. 함수 호출 시, 매개변수가 배열인지 체크 됨
*/

 

2. any보다 더 정확하게 모델링 할 수 있도록 any[] 또는 {[id: string]: any} 또는 () => any처럼 구체적인 형태를 사용해야한다.
(그냥 any 보다는 조금이라도 더 구체화시키려 노력하자.)

 


아이템 40. 함수 안으로 타입 단언문 감추기

요약

  • 타입 단언문은 일반적으로 타입을 위험하게 만들지만 상황에 따라 필요하기도 하고 현실적인 해결책이 되기도 한다. 불가피하게 사용해야 한다면, 정확한 정의를 가지는 함수 안으로 숨기도록 한다.
    • 가능한 안전한 타입으로 구현하는 것이 이상적이나, 불필요한 예외 상황까지 고려해 가며 타입 정보를 힘들게 구성할 필요는 없다.
    • 함수 내부에서는 타입 단언 을 사용하고, 함수 외부로 드러나는 타입 정의를 명시하는 정도가 적절할 수 있다.
//예제 - 함수 캐싱하는 함수: 함수가 자신의 마지막 호출을 캐시(기억)하도록 만들어주는 함수
declare function shallowEqual(a: any, b: any): boolean;
function cacheLast<T extends Function>(fn: T): T {
    let lastArgs: any[] | null = null;
    let lastResult: any;

    return function(...args: any[]) {
        if (!lastArgs || !shallowEqual(lastArgs, args)) {
            lastResult = fn(...args);
            lastArgs = args;
        }
        return lastResult;
    } as unknown as T;
}

//원본 함수 타입 T와 리턴하는 함수가 어떤 관련이 있는지 모르기 때문에 에러 발생!
//그려나 우리는 두 함수가 '같은 매개변수를 주면 같은 값을 반환하는' 함수여서 
//동일하게 취급해도 문제 없다는 것을 알기때문에 단언문 사용해도 괜찮다.

 

아이템 1 ~ 5 정리본

아이템 1: 타입스크립트와 자바스크립트의 관계 이해하기

모든 자바스크립트는 타입스크립트지만, 모든 타입스크립트는 자바스크립트가 아니다.

let city = 'new york city';
console.log (city.toUppercase());
// 'toUppercase* 속성이 'string* 형식에 없습니다.
// 'toUpperCase1 을(를) 사용하시겠습니까?

city 변수가 문자열이라는 것을 알려주지 않아도 타입스크립트는 초깃값으로부터 타입을 추론한다.

타입 추론은 ts에서 중요한 부분 !!

 

타입 시스템의 목표

  • 런타임에 오류를 발생시킬 코드를 미리 찾아내는 것

하지만, 타입 체커가 모든 오류를 찾아내지는 않음

const states = [
    {name: 'Alabama'f capital: 'Montgomery'},
    {name: 'Alaska', capital: ’Juneau'},
    {name: 'Arizona', capital: 'Phoenix'},
];

for (const state of states) {
    console.log(state.capitol);
}

// undefined
// undefined
// undefined

for (const state of states) {
    console.log(state.capitol);
    // ----------- 'capitol' 속성이 ... 형식에 없습니다.
    // 'capital'을(를) 사용하시겠습니까?
}
  • 타입스크립트는 어느 쪽이 오타인지 판단하지 못한다.
  • 오류의 원인은 추측할 수 있겠지만 항상 정확하지는 않다.

⇒ 명시적으로 states를 선언하여 의도를 분명하게 하자.

 

interface 사용

interface State {
    name: string;
    capital: string;
}
const states: State[] = [
    {name: 'Alabama', capitol: 'Montgomery'}
                                        // ------------
    {name: 'Alaskar', capitol: 'Juneau'},
                                        // ------------
    {name: 'Arizona', capitol: 'Phoenix'},
                                        // ------------
    // 개체 리터럴은 알려진 속성만 지정할 수 있지만
    // 'State' 형식에 'capitol'이(가) 없습니다.
    // 'capital'을(를) 쓰려고 했습니까?
    // ...
];
for (const state of states) {
    console.log(state.capital);
}

의도를 분명하게 하여, 오류가 어디서 발생했는지 찾을 수 있음 !

 

아이템 2: 타입스크립트 설정 이해하기

command line 으로 설정

$ tsc —noImplicitAny program.ts

or

tsconfig.json 파일로 설정

{
    "compilerOptions": {
        "noImplicitAny": true
    }
}

위 설정을 통해 타입을 제어할 수 있다.

 

strictNullChecks

  • null과 undefined가 모든 타입에서 허용되는지 확인하는 설정
// strictNullChecks = true
const x: number = null; // 정상, null은 유효한 값

// strictNullChecks = false
const x: number = null; // 비정상, 'null' 형식은 'number' 형식에 할당할 수 없습니다.

 

아이템 3: 코드 생성(컴파일)과 타입이 관계없음을 이해하기

타입스크립트 컴파일러 역할 독립적으로 수행

  1. ts/js를 브라우저에서 동작할 수 있도록 구버전의 js로 트랜스파일 (번역 + 컴파일)
  2. 타입 오류 체크

따라서, ts가 js로 변환될 때 코드 내의 타입에는 영향을 주지 않는다.

또한, 자바스크립트의 실행 시점에도 타입은 영향을 미치지 않는다.

타입 오류가 있는 코드도 컴파일이 가능함
컴파일은 타입 체크와 독립적으로 동작하기 때문에, 타입 오류가 있는 코드도 컴파일 가능
cf. C나 자바의 경우 타입 체크와 컴파일이 동시에 이루어짐

오류가 있을 때 컴파일하지 않으려면 ..

  • tsconfig.json에 noEmitOnError를 설정하거나 빌드 도구에 적용한다.

런타임에는 타입 체크가 불가능하다

instanceof 체크는 런타임에 일어나지만, Rectangle은 타입이기 때문에 런타임 시점에서 아무것도 할 수 없다.

cf ) instanceof + Class명

 

런타임에 타입을 지정하기 위해서는 ?

  • 런타임에 타입 정보를 유지하는 방법

1. 속성이 존재하는지 체크한다.

2. 런타임에 접근 가능한 타입 정보를 명시적으로 저장하는 ‘태그’ 기법이 있다.

 

아이템 4: 구조적 타이핑에 익숙해지기

  • 자바스크립트는 덕 타이핑 (duck typing) 기반으로, 타입스크립트는 이를 모델링하기 위해 구조적 타이핑을 사용함

덕 타이핑

  • 타입을 미리 정하는게 아닌 실행이 되었을 때 해당 method를 확인하여 타입을 정함

구조적 타이핑

  • y가 최소한 x와 동일한 멤버를 가지고 있다면 x와 y는 호환된다.
type Person = {
    name: string
};

let person: Person

const employee = {
    name: 'Anecdote',
    job: 'Developer',
}

person = employee; // OK

 

employeePerson 타입에 필요한 name 속성을 가지고 있기 때문에 그 외의 속성이 있더라도 person의 값으로 할당할 수 있다.

 

타입스크립트는 다른 인터페이스여도 이해할 수 있을만큼 영리하다.

interface Vector2D {
    x: number;
    y: number;
}

function calculateLength(v: Vector2D) {
    return Math.sqrt(v.x * v.x + v.y * v.y);
}

interface NamedVector { // extends Vector2D로도 가능
    name: string;
    x: number;
    y: number;
}

const v: NamedVector = { x: 3, y: 4, name: 'Zee' };
calculateLength(v); // 정상, 결과는 5

 

아이템 5: any 타입 지양하기

any 타입 사용시 타입 체커와 타입스크립트 언어 서비스를 무력화시킨다.

따라서 최대한 사용을 피할 것

  1. any 타입에는 타입 안정성이 없다.
    age는 number 타입으로 선언되었지만, as any를 사용하면 string 타입을 할당할 수 있게 된다.
    따라서 아래처럼 선언될 가능성이 있다.
    ex. age += 1 // 런타임에 정상, age는 “121”
  2. any 타입에는 언어 서비스가 적용되지 않는다.
    => ts 모토:
    확장 가능한 자바스크립트

타입이 있다면 타입스크립트 언어 서비스는 자동완성 기능과 적절한 도움말을 제공한다.

그러나 any 타입은 제공하지 않는다.

이름 변경 서비스도 제공하지 않는다.

interface Person {
    first: string;
    last: string;
}

const formatName = (p: Person) => `${p.first} ${p.last}`
const formatNameAny = (p: any) => `${p.first} ${p.last}`

--- 여기서 Rename Symbol을 클릭 해 이름 변경 ---
프로젝트 내의 심벌 이름이 모두 변경됨

3. any 타입은 코드 리팩터링할 때 버그를 감춘다.

4. any는 타입 설계를 감춰버린다.

5. any는 타입시스템의 신뢰도를 떨어뜨린다.

4가지 모두 공통적으로 고차함수이다.
*고차 함수: 자신의 매개변수에 함수를 전달 받는 것

 

1. forEach

function forEach(predicate, thisArg) {
	for(let i=0; i<a.length; i++) {
    	predicate(a[i], i); // 반복해서 호출하는 것
    }
}

a = [10, 11, 12, 13, 14, 15];
a.forEach(function(v, i){ // 인자로 callback함수를 넘겨준다.
	console.log(v, i)
});

 

2. map

원본 배열을 하나하나 탐색하면서 새로운 배열을 생성한다.

function map(predicate, thisArg) {
	// 새로운 배열 생성
	let list=[];
    for(let i=0; i<a.length; i++) {
    	list.push(predicate(a[i], i));
    }
    return list;
}

a = [10, 11, 12, 13, 14, 15]
let answer = a.map(function(v, i) {
	return v*v;
});

console.log(answer); // [100, 121, 144, 169, 196, 225]

새로운 배열을 넘겨받을 수 있고, 중요한 것은 새로운 배열과 원본 배열의 길이는 같다. 

let answer = a.map(function(v, i){
	if (v%2 == 0) return v; // [10, undefined, 12, undefined, 14, undefined]
};

만약 위와 같이 11, 13, 15와 같이 홀수인 값은 return되지 못해 undefined 값을 푸시한다.

 

3. filter

map과 같이 새로운 배열을 생성하지만, 말그대로 filter 즉, 걸러준다고 생각하자!

filter는 원하는 원소만 배열을 생성해서 return 해준다.

let answer = a.map(function(v, i){
	if (v%2 == 0) return v; // [10, 12, 14]
};

따라서 map과는 다르게 원본과 생성된 배열의 길이가 같지 않다

 

 4. reduce

배열의 각 요소를 순회하며 callback 함수의 실행 값을 누적하여 하나의 결과 값을 반환한다.

function reduce(predicate, arg) {
	let result = arg; // 넘어온 값을 초기화한다.
    for(let i=0; i<a.length; i++) {
    	result = predicate(result, a[i]);
    }
    return result;
}
a = [10, 11, 12, 13, 14, 15]
answer = a.reduce(function(accumulate, v){
	return accumulate + v;
}, 0);

 

처음에 result에 0이 들어가고,

0 + 10 = 10

10 + 11 = 21

21 + 12 = 33 
...

결국 10 ~ 15를 더하게 된다!

 

 

 

*출처: 해당 코드들은 인프런 [자바스크립트 알고리즘 문제풀이 입문]을 들으며 수강한 내용입니다.

+ Recent posts