[TypeScript] 타입스크립트의 타입 시스템 (2)

김서현·2023년 3월 24일
0

TypeScript 스터디

목록 보기
3/4
post-thumbnail

📌 타입 단언보다는 타입 선언을 사용하기

타입 단언과 타입 선언

  • 타입을 부여하는 방법 두 가지이다.
interface Person {name: string};

const alice: Person = { name: 'Alice' }; // 타입 선언
const bob = { name: 'Bob' } as PErson; // 타입 단언
  • 타입 선언 : 그 값이 선언된 타입임을 명시

  • 타입 단언 : 타입스크립트가 추론한 타입이 있더라도 Person(단언한) 타입으로 간주

  • 타입 선언 -> 할당되는 값이 해당 인터페이스를 만족하는지 검사
    타입 단언 -> 강제로 타입을 지정해서 타입 체커가 오류를 무시하도록 만듬
    결론 : 타입 단언 보다는 타입 선언을 사용하자!

    ➕ 알아가기
    원래 타입 단언의 문법은 const bob = <Person>{}이다.
    그러나 <Person>.tsx파일에서 컴포넌트 태그로 인식되기 때문에 현재는 잘 쓰이지 않는다.


타입 단언이 꼭 필요한 경우

  • 타입 체커가 추론한 타입보다 우리가 판단하는 타입이 더 정확할 때

ex1) DOM 엘리먼트 -> 타입스크립트는 DOM에 접근할 수 없기 때문에 #myButton이 버튼 엘리먼트인지 알지 못한다!

const divEl = document.querySelector("#myButton");
if (divEl) {
  divEl.addEventListener("click", (e) => {
    const button = e.currentTarget as HTMLButtonElement;
    button;
  });
}

ex2) null이 아님을 단언하는 경우 : 접미사 !는 그 값이 null이 아니라는 단언문으로 해석됨. <-> 우리가 자주 쓰는 접두사 !은 boolean의 부정문

const elNull = document.getElementById("foo"); //HTMLElement | null
const ell = document.getElementById("foo")!; //null이 아님을 단언 --> HTMLElement

ex3) 임의의 타입 간의 변환

  • 타입 간의 변환은 변환하려는 타입 A가 기존 타입 B의 부분 집합인 경우에 사용할 수 있다.
const el = document.body as Person; // 오류 : Person과 HTMLElement는 서로의 서브타입이어야 가능함.
// 이 오류를 해결할 때 unknown 타입 사용 가능. unknown은 모든 타입의 서브타입이기 때문
const el = document.body as unknown as Person;

❗ 그러나 강제로 임의의 타입 간의 변환을 가능케 하는 것이므로 이런식의 unknown 사용은 권장 ❌


화살표 함수의 반환 타입

  • 반환 타입이 Person[] 이기를 원하는 상황
interface Person {
  name: string;
}

// Person[]를 원했지만 결과는 {name: string}[]
const people = ["alice", "bob", "jan"].map((name) => ({ name }));
  • 여기에 타입 단언을 사용하면? ❌
    -> 에러는 없지만 어디선가 쓸 때 런타임에 문제 발생할 것!
const people = ["alice", "bob", "jan"].map((name) => ({} as Person));
  • 타입과 함께 변수를 선언하는 것이 가장 직관적이다.
const people_ = ["alice", "bob", "jan"].map((name) => {
  const person: Person = { name };
  return person;
});

🔽좀 더 간결하게 하자면?🔽

const people: Person[] = ["alice", "bob", "jan"].map((name) => ({ name }));

📌 객체 래퍼 타입 피하기

  • 기본형 값에 대한 일곱 가지 타입 : string, number, boolean, null, undefined, symbol, bigint --> 불변, 메서드를 가지지 않는다.
  • 객체 타입 : Number, Boolean, Symbol, BigInt --> 메서드를 가진다.

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

'primitive'.charAt(e)
// > "m"
  • JS는 기본형과 객체 타입을 서로 자유롭게 변환하는데, JS에 메서드를 가지는 String '객체' 타입이 정의되어 있기에,
  • 기본형(string)에 charAt과 같은 메서드를 사용할 때, 기본형을 String 객체로 래핑(wrap)하고, 메서드를 호출하고, 마지막에 래핑한 객체를 버린다
    👉 number -> Number, boolean -> Boolean, symbol --> Symbol, bigint --> BigInt ( null과 undefined는 객체 래퍼가 없다.)
    👉 ❗ 이런 성질에 의해 어떤 속성을 기본형에 할당한다면 그 속성이 사라지는 상황을 겪는다.
x = "hello" //string
x.language = 'English' //String 객체로 변환
x.language // undefined : 객체 버려짐
  • string은 String에 할당할 수 있지만, String은 string에 할당할 수 없다
function getStringLen(foo: String) {
  return foo.length;
}

// 둘다 정상
getStringLen("hello");
getStringLen(new String("hello"));

// 오류 : string 을 매개변수로 받는 메서드에 String 객체를 전달하면 문제가 발생
function isGreeting(phrase: String) {
  return ["hello", "good day"].includes(phrase);
}

-> 기본형 타입을 객체 래퍼에 할당하는 구문은 쓰지 않는게 좋다!
➕) 그러나 BigInt랑 Symbol은 기본형을 생성하므로 사용해도 좋음.

결론 : 타입스크립트 객체 래퍼 타입은 지양하고, 기본형 타입을 사용하자!


📌 잉여 속성 체크의 한계 인지하기

  • 잉여 속성 체크 : 해당 타입 이외의 속성은 없는지 확인
interface Room {
  numDoors: number;
  ceilingHeightFt: number;
}

const r: Room = {
  numDoors: 1,
  ceilingHeightFt: 10,
  elephant: "present", // 오류 : 'Room' 형식에 'elephant'가 없습니다.
};

👉 잉여 속성 체크를 통해서 구조적 타입 시스템에서 발생할 수 있는 중요한 종류의 오류를 잡을 수 있다!
👉 잉여 속성 체크를 통해서 의도와 다르게 작성된 코드까지 찾을 수 있다. ex) darkmode가 아닌 darkMode로 쓰는 경우

❓ 그런데 구조적 타이핑 관점에 의해 오류가 발생하지 않아야 하는거 아니야 ?!
▶ 임시 변수를 도입해보자! obj 객체는 Room 타입에 할당 가능.

const obj = {
  numDoors: 1,
  ceilingHeightFt: 10,
  elephant: 'present',
};
const r: Room = obj; // 정상

obj 타입은 {numDoors: number; ceilingHeightFt: number; elephant: string}으로 추론되므로 Room 타입의 부분집합을 포함해서 Room에 할당 가능하게 되는 것


잉여 속성 체크는 언제 일어날까?

  • 객체 리터럴변수에 할당하거나, 함수에 매개변수로 전달할 때
    👉 위 obj 예제 같은 경우, 임시 변수를 도입하여 결국 r에 할당하는 obj가 객체 리터럴이 아니기 때문에 잉여속성체크 없이, 구조적 타이핑 관점에 의해서 정상 처리 되는 것이다.
  • 타입 단언문을 사용할 때도 적용되지 않는다.

공통 속성 체크

  • 약한 타입 : interface가 선택적 속성만 가지는 타입
  • 약한 타입은 값 타입과 선언 타입에 공통된 속성이 있는지 확인하는 별도의 체크를 수행한다! -> 잉여 속성 체크와 다르게, 약한 타입과 관련된 할당문마다 수행된다.
interface LineChartOptions {
  logscale?: boolean;
  invertedYAxis?: boolean;
  areaChart?: boolean;
}
const opts = { logScale: true};
const o : LineChartOptions = opts; //오류 : '{ logScale: boolean; }' 유형에 'LineChartOptions' 유형과 공통적인 속성이 없습니다.

✅ 결론 : 임시 변수를 도입했을 때 잉여 속성 체크가 일어나지 않는 한계를 조심하자.

2개의 댓글

속성을 기본형에 할당하면 그 속성이 없어진다니..!! 타입스크립트에서는 객체 타입 쓰는 걸 조심히 해야겠네요..!! 그리고 임시 변수를 썼을 때, 잉여 속성 체크를 우회할 수 있다는 것이 장점이자 단점이 될 수 있을 것 같군요!!! 좋은 글 잘 읽었습니다 :)

1개의 답글