0%

타입 가드

#타입 가드란?
일부 스코프에서 타입을 보장하는 런타임 검사를 수행하는 표현식. 타입 가드를 정의하여면 반환 타입이 타입 명제(type predicate)인 함수를 정의 하기만 하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
interface Bird {
fly();
eat();
}
interface Fish{
swim();
eat();
}
function getSmallPet: Fish | Bird{
...
}

let pet = getSmallPet();

//타입 가드
//타입 명제는 argName is Type형태
//argName은 현재 함수에서 사용한 매개변수의 이름을 사용해야한다.
function isFish(pet:Fish | Bird):pet is Fish{
return (<Fish>pet).swim !== undefined;
}


if(isFish(pet)){ //isFish(pet)이 true이면 pet은 Fish이다.
pet.swim();
}else{
pet.fly();
}

typeof 타입 가드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function isNumber(x:any):x is number{
return typeof x === "number";
}
function isString(x:any):x is string{
return typeof x === "string";
}
function padLeft(value:string,padding: string | number){
if(isNumber(padding)){
return Array(padding+1).join(" ").value;
}
if(isString(padding)){
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'`);
}

다음과 같은 순반복 작업을 피하기 위해 , 타입 스크립트에서 인라인 검사를 통해 타입가드를 통해 지원한다.

인라인은 함수의 내용만을 그대로 옮겨 놓은 듯이 작성하는 것을 의미

1
2
3
4
5
6
7
8
9
function padLeft(value:string,padding:string | number){
if(typeof padding === "number"){
return Array(paddding+1).join(" ").value;
}
if(typeof padding === "string"){
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'`)
}

typeof을 사용한 타입가드느 typeof === “typename” 또는 typeif !== “typename”두가지 형태를 지원하고 ,typename은 [number ,string , boolean,symbol]중 하나이어야한다.

instanceof 타입 가드

클래스를 사용할 때는 instanceof을 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
interface Padder{
getPaddingString():string;
}

class SpacePadder 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 SpacePadder(4):
new StringPadder(" ")
};
let padder:Padder = getRandomPadder();

if(padder instanceof SpacePadder){
padder;

if(padder instanceof StringPadder){
padder;
}

작성방식은variableName instanceof constructorName.

  1. 타입이 any가 아닌 경우 함수의prototype 프로퍼티 타입
  2. 해당 타입의 생성자 스그니처에 의해 반환된 타입의 결합

선택적 매개변수와 프로퍼티

–strictNulCheck 를 선택적 매개 변수와 함께 쓰면 자동으로 | undefined를 추가 합니다.

1
2
3
4
5
6
7
function f(x:number,y?:number){
return x +(y || 0);
}
f(1,2);
f(1);
f(1,undefiend);
f(1,null) // 오류 , 'null'은 'number | undefined'에 할당할 수 없습니다.

선택적 프로퍼티도 동일합니다.

1
2
3
4
5
6
7
8
9
10
class C{
a:number;
b?:number;
}
let c = new C();
c.a = 12;
c.a = undefined; //오류 undefined는 'number'에 할당 할 수 없다.
c.b = 13;
c.b = undefined //ok;
c.b = null //오류 'null'은 'number | undefined'에 할당 할 수 없다.

타입 가드와 타입 단언

Nullable 타입은 유니온 타입으로 구현 되기 때문에 타입가드를 사용하여 null을 제거해야합니다.

1
2
3
4
5
6
7
8
9
10
11
function f(sn:string | null):string{
//case 1
if(sn === null){
return 'result';
}
else{
return sn;
}
//case 2
return sn || 'default';
}

컴파일러가 null 또는 undefined를 제거할 수 업슨 경우에 타입 단언 연산자를 사용하여 수동으로 제거해야합니다. 연산자는 ! 입니다. v!은 v!의 null과 undefined을 제거합니다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function broken(name:string | null):string{
//namedl null일 수도 있다는 에러가 발생
function postfix(epithet:string){
return name.chartAt(0)+'. the '+ epithet; //오류 'name'이 null일 수 있다.
}
//name이 null이어도 여기서 "Bob"으로 바뀝니다.
name = name || 'Bob';
return prostfix("graet")''
}

function fixed(name:string | null ):string{
function postfix(epithet:string){
return name!.charAt(0)+ '. the '+ epithet;
}
name = name || "Bob";
return postfix("great");
}

컴파일러가 name이 null 일수 있다고 판단하는 이유는 , 외부 함수에서 호출한 경우 중첩된 함수에 대한 모든 호출을 추적하는 것이 불가능하기 때문입니다. (즉시 실행함수 IIFE의 경우 가능)

1
2
3
4
5
6
7
function broken(name:string | null):string{
name = name || "Bob";
return(function postfix(epithet:string){
//즉시 실행 함수이기 때문에 name이 null이 아니라는 것을 알고 있다.
return name.chatAt(0)+ '. the' + epithet;
})("great");
}

타입 단언( type Assertion)

타입 스크립트가 컴파일러 타입을 실제 런타입에 존재 할 변수의 타입과 다르게 추론하거나 너무 보수적이 ㄴ추론을 하는 경우에 프로그래머가 수동으로 컴파일러한테 특정 변수에 대해 타입 힌트를 주는것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Character{
hp:number;
runAway(){

}
isWizard(){

}
isWarrior(){

}
}
class Wizrd extends Character{
fireBall(){

}
}
class Warrior extends Character{
attack(){}
}
function battle(character: Character) {
if (character.isWizard()) {
character.fireBall(); // Property 'fireBall' does not exist on type 'Character'.
} else if (character.isWarrior()) {
character.attack(); // Property 'attack' does not exist on type 'Character'.
} else {
character.runAway();
}
}

다음 코드는 컴파일 에러가 발생한다. Character 클래스에는 fireBall ,attack케소드가 선언되어 있지 ㅇ낳기 때문 isWizard라는 메소드를 통해 그 캐릭터가 Wizard인스턴스라는 것을 보장 할수 있다면 FireBall 메소드를 사용할 수있다.

1
2
3
4
5
6
7
8
9
function battle(character:Character){
if(character.isWizard()){
(character as Wizard).fireBll(); //pass;
}else if(character.isWarrior()){
(character as Warrior).attact(); //pass
}else{
character.runAway();
}
}

타입 단언은 크게 두가지 와 as Type 두가지 방법이 존재하는데

1
2
(<Wizard>character).fireBall();
(character as Wizard)/fireBall();

보통 키워드가 좀더 깔끔해 보이지만 React에서 jsx 문법을 사용할 때 와 문법이 겹칠 수 잇기 때문에 불편한 면이 존재한다.

타입가드는 타입으 ㄹ좀더 갈끔하게 할 수 잇도록 도와 준다. 앞서 타이 ㅂ단언에서 소개한 예제에서는 isWizard 라는 메소드로 해당 인스턴스가 해당 타입이라는 사실을 확정했다. 하지만 이건 런타임에서만 알 수 있는 사실이고 TypeScript 컴파일러는 알 수 없었다. 타입 가드는 이러한 런타임에서의 타입 체크를 컴파일러에게 알려주는 기능이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Character{
isWizard():this is Wiazrd{
return this instanceof Wizard;
}
isWarrior():this is Warrior{
return this instanceof Warrior;
}
}

function battle(character :Character){
if(character.isWizard()){
character.fireBall(); //ok
}else if(character.isWarrior()){
character.attack() //ok;

}else{
character.runAway();
}
}
//이제 별도의 타입 단언 문법없이도 if 블록안에서 character가 Wizard나 Warrior로 자 ㄹ추론이 된다. 그리고 사실 instanceof와 typeof같은 오퍼레이터로 일종의 타입 가드이다
1
2
3
4
5
6
7
8
9
function doSomething(val:string | number){
if(typeof val === 'number'){
val.toFixed(); //pass val은 number로 타입 추론

}else{
//union타입에서 'number'는 이미 통과 했으므로 자동으로 'string'으로 추론됨
val.toLowerCase(); //pass, val은 string 타입으로 추론됨
}
}