이펙티브 타입스크립트 2-9 ~ 2-11

이승훈·2024년 3월 17일
1
post-thumbnail

2-9 타입 단언보다는 타입 선언 사용하기

interface Person { name: string};

const alice: Person = { name: 'Alice' } // <- 타입 선언
const bob = { name: 'Bob' } as Person // <- 타입 언

타입단언의 경우 타입스크립트가 추론한 타입이 있더라도 Person 타입으로 간주한다.

const alice: Person = {}; // <- 타입 에러 발생 
const bob = {} as Person // <- 타입 오류 없음

타입 선언은 할당되는 값이 해당 인터페이스를 만족하는지 검사한다.
타입 단언은 강제로 타입을 지정했으니 타입 체커에게 오류를 무시하라고 하는것이다.

타입 단언이 꼭 필요한 경우가 아니라면, 안전성 체크도 되는 타입 선언을 사용하는 것이 좋다.

타입 단언은 사용하는것이 타당한 경우

document.querySelector('#myButton').addEventListner('click', e => {
	e.currentTarget // 타입은 eventTarget
  	const button = e.currentTarget as HTMLButtonElement;
  	button  // 타입은 HTMLButtonElement
})

타입스크립트는 DOM에 접근할 수 없기 때문에 #myButton이 버튼 엘리먼트인지 모른다.
그러기에 이벤트의 currentTarget이 같은 버튼이어야 한다는것도 모른다.
개발자가 타입스크립트가 모르는 정보를 가지고 있기 때문에 여기서는 타입 단언을 사용하는것이 타당하다.

접미사 !

const elNull = ducument.getElementById('foo') // 타입은 HTMLElemen | null
const el = ducument.getElementById('foo')! // 타입은 HTMLElemen

접미사 !는 그 값이 null이 아니라는 단언문으로 해석된다.

타입 단언문으로 임의의 타입 간에 변환을 할 순 없다.
A가 B의 부분집합인 경우 타입 단언문을 사용해 변환할 수 있다.

HTMLElement는 HTMLElement | null 의 서브타입이기 때문에 역시 동작한 것이다.
그리고 Person은 {}의 서브타입이므로 동작한 것이다.

interface Person { name: string; }
const body = document.body;
const el = body as Person; // 이런거 안된다고 이러면 타입 에러 뜬다고

2-10 객체 래퍼 타입 피하기

javascript에는 객체 이외에도 기본형 값들에 대한 일곱가지 타입이 있다.
(string, number, boolean, null, undefined, symbol, bigint)
기본형들은 불변(immutable)이며 메서드를 가지지 않는다는 점에서 객체와 구분된다.

그런데 기본형인 string의 경우 메서드를 가지고 있는 것처럼 보인다.

'lalalalala'.charAt(3)

하지만 사실 chatAt은 string의 메서드가 아니다.
string을 사용할 때 javascript 내부적으로 많은 동작이 일어난다.

string '기본형'에는 메서드가 없지만, javascript에는 메서드를 가지는 String'객체' 타입이 정의되어 있다.
javascript는 기본형과 객체 타입을 서로 자유롭게 변환한다.
string 기본형에 charAt같은 메서드를 사용할 때 javascript는 기본형을 String 객체로 래핑하고, 메서드를 호출하고, 마지막에 래핑한 객체를 버린다.

타입스크립트에서 타입을 선언할 때 string 대신 String을 잘못 기입하면 안된다.

string을 매개변수로 받는 메서드에 String 객체를 전달하는 순간 문제가 발생하기 때문이다.

function isGreeting(phrase: String){
	return [
    	'hello',
      	'good day'
    ].includes(phrase);
  	// ~~~~~~~~~
  	// 'String' 형식의 인수는 
  	// 'string' 형식의 매개변수에 할당될 수 없습니다.
  	// 'string'은 개본 개체지만 'String'은 래퍼 개체입니다. 
  	// 가능한 경우 'string'을 사용하세요.
}

타입스크립트 객체 래퍼 타입은 지양하고, 대신 기본형 타입을 사용해야한다.
String 대신 string, Number 대신 number...요로케

2-11 잉여 속성 체크의 한계 인지하기

타입이 명시된 변수에 객체 리터럴을 할당할 때 타입스크립트는 해당 타입의 속성이 있는지, 그리고 '그 외의 속성은 없는지' 확인한다.

interface Room {
  numDoors: number;
  ceilingHeightFt: number;
}

const r: Room = {
  numDoors: 1,
  ceilingHeightFt: 10,,
  elephant: 'present'
  //~~~~~~~~~~객체 리터럴은 알려진 속성만 지정할 수 있으며 
  //		  'Room' 형식에 'elephant'가 없습니다.
}

Room 타입에 elephane가 있는게 어색하긴 하지만, 구조적 타이핑관점에서 보면 오류가 발생하지 않아야한다.

const obj = {
  numDoors: 1,
  ceilingHeightFt: 10,
  elephant: 'present'
}

const r: Room = obj // 정상

obj의 추론되는 타입은 Room 타입을 부분집합으로 포함하므로 Room에 할당 가능하며 타입 체커도 통과한다.

첫번째 예제의 경우에는 구조적 타입 시스템에서 발생할 수 있는 중요한 오류를 잡을 수 있도록 '잉여 속성 체크'라는 과정이 수행되었다.
왜 객체 리터럴을 직접 작성할 때는 잉여 속성 체크를 하고 변수로서 할당 할 땐 할당가능 검사를 할까(구조적 타입시스템으로서 부분집합으로서 포함하고 있으면 문제 없을까)?

interface Options {
  title: string;
  darkMode?: boolean;
}
function createWindow(options: Option){
  if(options.darkMode){
    setDarkMode();
  }
  // ...
}
createWindow({
  title: 'Spider Solitaire',
  darkmode: true,
  //~~~~~~~~~~~~객체 리터럴은 알려진 속성만 지정할 수 있지만
  //            'Options' 형식에 darkmode 가 업습니다.
  //			darkMode를 쓰려고 했습니까?
})

만일 구조적 타입체크만 하였다면 위의 createWindow 함수를 호출 및 실행할 때 아무런 문제가 없다.
왜냐면 함수의 매개변수로 입력된 객체의 타입은 Options타입을 부분집합으로서 포함하고 있기 때문이다.

title을 가지는 모든 객체는 Options타입의 범위에 속한다.

그러나 잉여 속성 체크를 이용하면 기본적으로 타입 스스텝의 구조적 본질을 해치지 않으면서도 객체 리터럴에 알 수 없는 속성을 허용하지 않음으로써, 앞에서 다룬 문제점을 방지할 수 있다.

profile
Beyond the wall

0개의 댓글