타임 시스템 강화
타입 좁히기
타입 추론
타입 단언
집합의로서의 타입
서로소 유니온 타입
타입 좁히기
타입 가드
특정 스코프 내에서 값의 타입을 좁히는, 즉 타입 좁히기를 유발하는 표현을 타입가드 (type guard)라 부르며, 크게 두 종류로 나뉜다. 한 종류는 제어 흐름 분석 (control flow analysis)을 통해 타입을 좁히는 가드들이고, 다른 하나는 프로그래머가 값을 어떤 타입으로 좁혀햐 하는지 직접 명시할 수 있는 수단인 사용자 정의 타입 가드 (user defined type guard) 다.
제어 흐름 분석
if, else, if else
while, for
switch, case
break, continue
return
컴파일러는 이런 제어 구조로부터 코드의 특정 시점에서 프로그램이 갖는 상태에 대한 정보를 얻어낼 수 있다. 그리고 컴파일러는 이러한 정보를 이용해 제어 흐름 분석을 진행해 특정 값의 타입의 좁혀낼 수 있다.
undefined / null 과의 비교
아래 if 문에서의 null 체크가 타입 가드로 동작한다
interface Animal {
ownerName: string | null;
}
function getOwnerName(animal: Animal): string {
if (animal.ownerName === null) {
return 'wildness';
} else {
// animal.ownerName 타입은 string
return animal.ownerName;
}
}
리터럴 타입과의 비교
switch-case 를 통해 타입을 좁힐 수 있다.
interface TeamLeader {
type: 'leader';
leadingSince: Date;
}
interface Newcomer {
type: 'newcomer';
major: string;
}
type Employee = TeamLeader | NewComer;
function doSomething(employee: Employee) {
switch (employee.type) {
case 'leader': {
// employee는 TeamLeader 타입
return employee.leadingSince;
}
case 'newcomer': {
// employee는 Newcomer 타입
return employee.major;
}
default: {
// employee는 never 타입
return null;
}
}
}
typeof 연산자
typeof의 반환값과 문자열을 비교한 결과를 타입 가드로 사용할 수 있다.
function padLeft(value: string, padding: string | number) {
if (typeof padding === "number") {
return Array(padding + 1).join(" ") + value;
}
if (typeof padding === "string") {
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}
typeof의 반환값
intanceof 연산자
instanceof 연산자는 값과 생성자를 받아 해당 값의 프로토타입 체인에 해당 생성자가 등장하는지를 확인한다
interface Padder {
getPaddingString(): string;
}
class SpaceRepeatingPadder implements Padder {
constructor(private numSpaces: number) { }
getPaddingString() {
return Array(this.numSpaces + 1).join(" ");
}
}
class StringPadder implements Padder {
constructor(private value: string) { }
getPaddingString() {
return this.value;
}
}
function getRandomPadder() {
return Math.random() < 0.5 ?
new SpaceRepeatingPadder(4) :
new StringPadder(" ");
}
// 이 시점에선 'SpaceRepeatingPadder | StringPadder'
let padder: Padder = getRandomPadder();
if (padder instanceof SpaceRepeatingPadder) {
padder; // SpaceRepeatingPadder 로 좁혀짐
}
if (padder instanceof StringPadder) {
padder; // StringPadder 로 좁혀짐
}
in 연산자
객체에 특정 속성이 존재하는지 여부를 확인할 때 사용된다
const obj = { a: 123 };
console.log('a' in obj); // true
console.log('b' in obj); // false
in 연산자도 타입가드로 사용 가능
interface Dog {
legs: 4;
bark(): void;
}
interface Insect {
legs: number;
creepy: boolean;
}
interface Fish {
swim(): void;
}
type Animal = Dog | Insect | Fish;
function doSomethingWithAnimal(animal: Animal) {
if ('legs' in animal) {
// animal은 Dog | Insect 타입
console.log(animal.legs);
} else {
// animal은 Fish 타입
animal.swim();
}
}
타입 추론
컴파일러는 타입 추론을 통해 명시적인 타입 표기 없이도 타입 정보를 이해할 수 있다.
let x: number = 3;
number 를 제거해도 number 타입일 것이라 추론해낸다.
let x = 3; // number 타입으로 추론
최적의 공통 타입
하나의 값에 대한 추론은 단순하다. 그렇다면 여러 값이 연관된 타입을 추론할 때는 어떨까?
interface Animal {
legs: number;
}
interface Dog extends Animal {
bark(): void;
}
interface Cat extends Animal {
meow(): void;
}
let dog: Dog;
let cat: Cat;
const dogAndCat = [dog, cat]; // ??
dogAndCat은 Dog 타입의 원소와, Cat 타입의 원소를 갖는 배열은 어떻게 추론해야할까?
타입스크립트는 최적 공통 타입 (best common type) 이란 접근법을 사용한다. 원리는 간단한데, 모든 가능한 타입의 유니온 타입을 사용하는 것이다.
dogAndCat의 타입을 Array<Dog | Cat> 으로 추론하는 식이다.
문맥 상의 타입
할당이 일어날 때, 타입 추론은 할당 받는 값(왼쪽 항)의 타입 뿐만 아니라 할당하는 값(오른쪽 항)의 타입에 대해서도 일어난다. 이렇게 추론된 타입을 문맥 상의 타입(contextual type)이라 부른다.
window.onmousedown = function(mouseEvent) {
console.log(mouseEvent.a);
};
이때, Window 인터페이스의 onmousedown 속성은 아래와 같이 정의 되어 있다.
interface MouseEvent {
/* ... */
/* button 속성 없음! */
}
interface Window {
/* ... */
onmousedown: (event: MouseEvent) => void;
}
따라서, 타입스크립트는 우변의 함수 ( event:MouseEvent => void) 타입일 것이라고 추론한다. 이 때 함수 내부에서 event.a 속성에 접근하는데, a 속성은 MouseEvent 타입에 존재하지 않으므로 타입 에러가 발생한다.
아래와 같이 mouseEvent 매개변수의 타입을 표기해주면 에러는 사라진다.
window.onmousedown = function(mouseEvent: any) {
console.log(mouseEvent.a);
};
타입 단언
타입 단언을 통해 컴파일러에게 특정 타입 정보의 사용을 강제할 수 있다.
타입 단언
타입스크립트 컴파일러는 타입 표기, 타입 좁히기와 타입 추론 등의 기법을 이용해 값의 타입을 판단한다. 하지만 때로는 컴파일러가 가진 정보를 무시하고 프로그래머가 원하는 임의의 타입을 값에 할당하고 싶을 수 있다. 이럴 떄 필요한 것이 바로 타입 단언(type assertion)이다.
타입 단언 문법
value as Type 문법을 사용해 value를 Type 타입으로 단언할 수 있다.
" value 가 어떤 타입인지는 내가 가장 잘 알아, 책임은 프로그래머인 내가 질테니,
네가 갖고 있는 정보는 다 무시하고 value를 Type 타입의 값이라고 생각하고 진행해 "
interface Dog {
legs: 4;
bark(): void;
}
interface Insect {
legs: number;
creepy: boolean;
}
interface Fish {
swim(): void;
}
type Animal = Dog | Insect | Fish;
function doSomethingWithAnimal(animal: Animal) {
(animal as Fish).swim();
}
Animal 타입의 값을 Finsh로 단언한다. 원래대로라면 Dog, Insect 타입에는 swim 메소드가 없다는 에러가 발생했겠지만, 타입 단언으로 인해 컴파일러는 animal 을 Fish 타입으로 해석하고, 타입 에러 없이 컴파일 된다.
주의할점은
타입 단언은 타입 에러를 없애줄 뿐 런타임 에러를 막아주지 않는다는 점이다. 오히려 그 반대인데, 컴파일러 타입에 잡을 수 있는 에러를 없앰으로서 원래대로면 생기지 않았을 런타임 에러를 발생시킬 수 있다. 실제로 위의 함수는 런타임에 Dog 혹은 Insect 타입 값을 받으면 에러가 발생될것이다.
any 타입 단언
값을 any 타입으로 단언함으로써 특정 값에 대한 타입 검사를 사실상 완전히 무효화 할 수 있다.
(3 as any).substr(0, 3);
위 코드는 실제로 실행한다면 런타임 에러가 발생하지만,타임 검사는 통과한다.
번거러운 타임 검사를 피할 수 있지만, any를 사용한 타입 단언은 어쩔 수 없는 경우를 제외하곤 피하는 것이 좋다. 타입스크립트를 사용하는 근본적인 이유는 런타임에 발생할 에러를 컴파일 타임에 방지하기 위해서 인데, any를 사용한 타입 단언은 그 의도에 정확히 반하기 때문이다.
아래와 같은 타입 단언에선 타입 에러가 발생한다. Dog 타입을 Insect로 취급할 수 없다는 것을 컴파일러가 알기 때문이다.
interface Dog {
legs: 4;
bark(): void;
}
interface Insect {
legs: number;
creepy: boolean;
}
const dog: Dog = { legs: 4, bark() { console.log('bark') } };
const insect: Insect = dog as Insect;
// error TS2352: Type 'Dog' cannot be converted to type 'Insect'.
// Property 'creepy' is missing in type 'Dog'.
이러한 제약은 any로 한번 타입 단언을 한 뒤, 그 값을 다시 Insect로 단언함으로서 피해갈 수 있다.
const insect2: Insect = (dog as any) as Insect; // ok
타입 단언이 막아주는 건 타입 에러 뿐이다. 절대 런타임 오류가 나지 않을 것이라는 확신이 있거나 런타임 에러가 나도 상관 없는 상황이 아니라면 이런 식으로 호환되지 않는 타입을 any를 거쳐 단언하는 일을 피하는게 좋다.
집합으로서의 타입
프로그래밍의 타입이 수학의 집합과 공유하는 성질, 그리고 타입을 집합으로 바라보는 관점
아래의 내용을 이해하고 나면 타입스크립트 뿐만 아니라 다른 여러 정적 타입 언어를 이해하는데 큰 도움이 될것이다.
https://ahnheejong.gitbook.io/ts-for-jsdev/06-type-system-deepdive/types-as-sets
6.4 집합으로서의 타입 - ts-for-jsdev
이런 대응 관계에서 알 수 있듯이, 프로그래밍의 타입은 수학의 집합에, 값은 원소에 대응한다. 타입은 값의 모음, 즉 집합인 것이다.
ahnheejong.gitbook.io
서로소 유니온 타입
https://ahnheejong.gitbook.io/ts-for-jsdev/06-type-system-deepdive/disjoint-union-type
6.5 서로소 유니온 타입 - ts-for-jsdev
서로소 유니온 타입의 브랜치 사이에 겹치는 값이 없다. 이 성질과 패턴 매칭을 사용하면 간결하고 직관적인 코드를 짤 수 있다. 이런 장점 때문에 하스켈, 스칼라, 러스트 등의 함수형 프로그래
ahnheejong.gitbook.io
참고
위의 모든 내용은 아래의 링크의 내용입니다.
https://ahnheejong.gitbook.io/ts-for-jsdev/06-type-system-deepdive/intro
6.0 들어가며 - ts-for-jsdev
이번 장에서는 타입 시스템이 동작하는 방식에 대해 보다 심도 있게 다룰 것이다. 가장 먼저, 타입스크립트가 코드 상의 여러 정보를 사용해 타입을 ‘좁혀 나가는’ 과정, 그리고 타입 표기 없
ahnheejong.gitbook.io
'TypeScript' 카테고리의 다른 글
[Typescript] 3. ts-for-jsdev 타입의 호환성 (0) | 2023.10.06 |
---|---|
[Typescript] 2. ts-for-jsdev 인터페이스와 클래스 공부 (0) | 2023.09.30 |
[Typescript] 1. ts-for-jsdev 타입스크립트 기초 문법 공부 (0) | 2023.09.28 |