[이펙티브 타입스크립트] 1장

김동영·2024년 1월 3일
1

타입 체크를 통과 하더라도 런타임에 오류가 발생 할 수 있음

const names = ['Alice', 'Bob'];
console.log(names[2].toUpperCase());
  • 타입 스크립트는 앞의 배열이 범위 내에서 사용될거라 가정했지만 실제로도 그러지 않았고 오류가 발생했습니다.
  • 타입스크립트가 이해하는값의 타입과 실제값과 차이가 존재하기때문
  • 타입시스템이 정적 타입의 정확성을 보장 할거 같지만 꼭 그러지않음



타입스크립트 설정

  • 타입 스크립트 컴파일러는 매우 많은 설정을 가지고 있다
    • 커맨드라인을 통해 설정할 수 있고 tsconfig.json 파일로도 가능하다
      • 가급적 설정파일을 사용해서 어떻게 사용할 것인지 프로젝트할 동료들과 정하는것이 좋다
    • noImplicitAnystrickNullChecks
      • noImplicitAny : 변수들이 미리 정의된 타입을 가져야 하는지 여부 제어
      • strickNullChecks: nullundefined가 모든타입에서 허용 되는지 확인하는 설정



코드 생성과 타입이 관계없음을 이해하기

타입 스크립트 컴파일러는 두가지 역할을 독립적으로 수행

  • 최신 타입스크립트/자바스크립트를 브라우저에서 동작할 수 있도록 구버전의 자바스크립트로 트랜스파일 합니다.
  • 코드의 타입 오류 체크 합니다.

독립적으로 수행되기때문에 타입오류가 있는 코드도 컴파일이 가능하다


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

interface Square {
  width: number;
}
interface Rectangle extends Square {
  height: number;
}
type Shape = Square | Rectangle;

function calculateArea(shape: Shape) {
  if (shape instanceof Rectangle) {
                    // 'Rectangle' only refers to a type,
                    //  but is being used as a value here
    return shape.width * shape.height;
                    // Property 'height' does not exist
                    // on type 'Shape'
  } else {
    return shape.width * shape.width;
  }
}
  • instanceof 체크는 런타임에 일어나지만 Rectangle은 타입이기 때문에 런타임 시점에 아무런 역할을 할 수 없습니다.
  • 타입 스크립트의 타입은 “제거 가능”이다 실제로 자바스크립트로 컴파일 되는 과정에서 모든 interface,type은 제거된다.

그래서 명확하게 하기 위해서는 아래 코드와 같이 런타임에 타입 정보를 유지하는 방법이 필요합니다 .


1. 속성 체크

interface Square {
  width: number;
}
interface Rectangle extends Square {
  height: number;
}
type Shape = Square | Rectangle;
function calculateArea(shape: Shape) {
  if ('height' in shape) {
    shape;  // Type is Rectangle
    return shape.width * shape.height;
  } else {
    shape;  // Type is Square
    return shape.width * shape.width;
  }
}
  • 속성 체크는 런타임에 접근 가능한 값에만 관련되지만 타입 체커 역시도 shape의 type을 Rectangle로 보정하기 때문에 오류가 사라짐

  1. 태그 기법
interface Square {
  kind: 'square';
  width: number;
}
interface Rectangle {
  kind: 'rectangle';
  height: number;
  width: number;
}
type Shape = Square | Rectangle;

function calculateArea(shape: Shape) {
  if (shape.kind === 'rectangle') {
    shape;  // Type is Rectangle
    return shape.width * shape.height;
  } else {
    shape;  // Type is Square
    return shape.width * shape.width;
  }
}

3. 타입(런타임 접근 불가)과 값(런타입 접근 가능)을 둘다 사용하는 기법(클래스)
class Square {
  constructor(public width: number) {}
}
class Rectangle extends Square {
  constructor(public width: number, public height: number) {
    super(width);
  }
}
type Shape = Square | Rectangle; // 타입으로 참조

function calculateArea(shape: Shape) {
  if (shape instanceof Rectangle) { // shape instanceof Rectangle 값으로 참조
    shape;  // Type is Rectangle
    return shape.width * shape.height;
  } else {
    shape;  // Type is Square
    return shape.width * shape.width;  // OK
  }
}
  • 인터페이스는 타입으로만 사용가능하지만 Rectangle을 클래스로 선언하면 타입과 값으로 모두 사용할 수 있습니다.


런타임 타입은 선언된 타입과 다를 수 있습니다

interface LightApiResponse {
	lightSwitchValue : boolean;
}

async function setLight(){
	const response = await fetch('/light');
	const result : LightApiResponse = await response.json();
  setLightSwitch(result.lightSwitchValue);
}

function setLightSwitch(value: boolean) {
  switch (value) {
    case true:
      turnLightOn();
      break;
    case false:
      turnLightOff();
      break;
    default:
      console.log(`I'm afraid I can't do that.`);
  }
}
  • 만약 lightSwitchValue의 값이 boolean이 아니라 문자열이라면 default가 발생하게 됩니다.
  • Typescript에서는 런타임타입과 선언된타입이 맞지 않을 수 있습니다.


타입스크립트 타입으로는 함수를 오버로드할 수 없습니다.

  • 타입 스크립트가 함수 오버로딩 기능을 지원하기는 하지만 온전히 타입수준에서만 동작합니다
  • 하나의 함수에 대해 여러개의 선언문을 작성할 수 있지만 구현체는 오직 하나뿐입니다
function add(a: number, b: number): number;
function add(a: string, b: string): string;

function add(a, b) {
  return a + b;
}
  • 두개의 선언문은 타입 정보만 제공 자바스크립트로 변환시 제거 되며 구현체만 남습니다.


타입스크립트 타입은 런타임 성능에 영향을 주지않습니다

  • 타입과 타입 연산자는 자바스크립트 변환시점에 제거되기 때문에 런타임의 성능에 아무런 영향을 주지 않는다.


구조적 타이핑에 익숙해지기

  • 자바스크립트는 본질적으로 덕 타이핑 기반입니다
    • 덕 타이핑이란? 객체가 어떤 타입에 부합하는 변수와 메서드를 가질 경우 객체를 해당 타입에 속하는 것으로 간주하는것 (상속 관계에 관계없이 타입 호환을 허용)
interface Vector2D {
  x: number;
  y: number;
}
function calculateLength(v: Vector2D) {
  return Math.sqrt(v.x * v.x + v.y * v.y);
}
interface NamedVector {
  name: string;
  x: number;
  y: number;
}
const v: NamedVector = { x: 3, y: 4, name: 'Zee' };
calculateLength(v);  // OK, result is 5

//NamedVector를 위한 calculateLength를 구현할 필요가 없음

하지만 이러한 특성때문에 아래와 같은 결과가 발생할 수도 있습니다.
interface Vector2D {
  x: number;
  y: number;
}
function calculateLength(v: Vector2D) {
  return Math.sqrt(v.x * v.x + v.y * v.y);
}
interface NamedVector {
  name: string;
  x: number;
  y: number;
}
interface Vector3D {
  x: number;
  y: number;
  z: number;
}
function calculateLengthL1(v: Vector3D) {
  let length = 0;
  for (const axis of Object.keys(v)) {
    const coord = v[axis];
    // 타입 호환때문에 v[axis]의 타입은 어떤 속성이 될 지 모르기때문에 number라고 확정지을 수 없음
    // 따라서 coord는 any type
    length += Math.abs(coord);
  }
  return length;
}
const vec3D = {x: 3, y: 4, z: 1, address: '123 Broadway'};
calculateLengthL1(vec3D);  // OK, returns NaN
  • 이러한 문제로 정확한 타입으로 객체를 순회하는것은 어렵기때문에 모든속성을 각각 더하는 구현이 더 낫습니다.

테스트를 작성할때는 구조적 타이핑이 유리합니다


interface Author {
  first: string;
  last: string;
}
interface DB {
  runQuery: (sql: string) => any[];
}
function getAuthors(database: DB): Author[] {
  const authorRows = database.runQuery(`SELECT FIRST, LAST FROM AUTHORS`);
  return authorRows.map(row => ({first: row[0], last: row[1]}));
}
  • getAuthors함수는 DB 인터페이스를 통해 데이터 베이스에 접근하므로 어떤 특정 데이터베이스에 종속적이지않아서 여러 종류의 데이터베이스와 동작할 수 있습니다.

any 타입 지양하기

  • any는 타입 안정성이 떨어진다.
  • any는 함수 시그니처를 무시해 버린다.
    • 호출하는쪽은 약속된 타입의 입력을 제공하고 함수는 약속된 타입의 출력 을 반환 합니다.
  • any타입은 언어 서비스가 적용되지 않는다.

아래와 같이 자동완성으로 속성이 나타나지 않는다.

  • any타입은 코드 리팩터링때 버그를 감춥니다.
interface ComponentProps {
  onSelectItem: (item: any) => void;
}
function renderSelector(props: ComponentProps) { /* ... */ }

let selectedId: number = 0;
function handleSelectItem(item: any) {
  selectedId = item.id; 
}

renderSelector({onSelectItem: handleSelectItem});

handleSelectItem의 매개변수 item이 any로 설정했기때문에 id의 유무에 상관없이 문제가 없다고 할것입니다.
하지만 id의 값이 존재하지 않는다면 타입체커를 통과함에도 불구하고 런타임에는 오류가 발생할 것입니다.
만약 any가 아닌 구체적인 타입을 사용했다면, 타입체커가 오류를 발견했을 것입니다.

profile
안녕하세요 프론트엔드개발자가 되고싶습니다

2개의 댓글

comment-user-thumbnail
2024년 5월 29일

요즘은 왜 글이 안 올라오나여?

답글 달기
comment-user-thumbnail
2024년 5월 29일

요즘은 왜 글이 안 올라오나여?

답글 달기