본문 바로가기
개발자 도전기/[STUDY] JS || TS

TypeScript | 강의 메모 | 타입 추론, 타입가드

by 답수 2022. 4. 5.
728x90
SMALL

 

 

※ 인프런 - 타입스크립트 시작하기(by 이재승) 강의를 기반으로 정리한 내용입니다.

 

 

타입 추론(Type Inference)

정적 타입 언의 단점은 타입을 정의하는데 많은 시간과 노력이 들기 때문에 생산성이 저하될 수 있다는 점이다. 하지만 타입스크립트의 경우 다양한 경우에 대해 타입 추론을 제공하기 때문에 꼭 필요한 경우에만 타입 정의를 할 수 있다. 여기서 말하는 타입 추론이란, 타입을 프로그래머가 따로 정의하지 않아도 자동으로 타입을 추론해주는 기능을 말한다. 타입 추론 덕분에 코드를 덜 작성하면서도 같은 수준의 타입 안정성을 유지할 수 있다는 점이 타입스크립트의 장점!

export {};

let v1 = 123;
let v2 = "abc";
v1 = 'a';       // number 형식에 string 할당 불가능
v2 = 456;       // string 형식에 number 할당 불가능
/**
 * v1, v2 변수에 타입을 정의하지 않았음에도 불구하고 v1은 숫자, v2는 문자열로 타입이 정의됨
 * 즉 자동으로 타입을 추론해 줌
 */

const v4 = 123;     // v4의 타입은 number가 되는 것이 아니고 123타입을 가지게 됨
const v5 = 'abc';
let v6: typeof v1 = 234;
let v7: typeof v4 = 234;    // v7은 123타입을 가지기 때문에 에러 발생
/**
 * let은 재할당이 가능한 반면 const는 재할당이 불가능하기 때문에 let변수보다 엄격하게 타입이 결정됨 (물론 let도 재선언은 불가)
 *  v7같은 경우도 123 타입만 사용 가능
 */


// 배열과 객체의 타입 추론
const arr1 = [10, 20, 30];      // 배열의 타입을 정의하지 않아도 숫자 원소들만 있으면 타입은 number로 정의됨
const [n1, n2, n3] = arr1;      // 비구조화(destructing) 할당을 하는 경우에도 각 변수는 자동으로 타입 추론이 됨
arr1.push('a');             // string 형식이기 때문에 에러

const obj = { id: 'asdf', age: 123, langauage: 'ko' };      // 객체 역시 자동으로 타입 추론이 됨
const { id, age, langauage } = obj;     // 마찬가지로 비구조화 할당을 했을 때도 자동으로 타입 추론 됨
console.log(id === age);    // error TS2367: This condition will always return 'false' since the types 'string' and 'number' have no overlap


// 인터페이스에서의 타입 추론
interface Person {
    name: string;
    age: number;
}
interface Korean extends Person {
    liveInSeoul: boolean;
}
interface Japanese extends Person {
    liveInTokyo: boolean;
}

const p1: Person = {name: 'mike', age: 23};
const p2: Korean = {name: 'mike', age: 25, liveInSeoul: true};
const p3: Japanese = {name: 'mike', age: 27, liveInTokyo: false};
const arr2 = [p1, p2, p3];      // const arr2: Person[]
const arr3 = [p2, p3];          // const arr3: (Korean | Japanese)[]
/**
 * 배열의 각 원소들의 타입이 다를 때는? 여러 가지 타입을 하나로 통합하는 과정을 거침
 * 다른 타입으로 할당 가능한 타입은 제거됨
 * arr1의 경우 Korean과 Japanese 타입은 둘 다 Person에 할당 가능하기 때문에 제거되고 Person만 남음
 * arr2은 서로 할당관계에 있지 않기 때문에 제거되는 것은 없고 유니온 타입으로 묶여서 나옴 
 */


// 함수의 타입 추론
function func1(a= 'abc', b = 10) {
    return `${a}, ${b}`;
}
func1(3, 6);    // 첫 번째 매개변수는 문자열 타입이기 때문에 에러
const v8: number = func1('a', 1);   // 함수는 문자열 타입을 반환하기 때문에 에러
/**
 * 매개변수에 타입을 정의하지 않아도 값의 타입에 따라 자동으로 타입 선언됨
 * 즉 a는 string, b는 number 타입으로 정의됨
 * 또한 함수가 문자열을 반환하기 때문에 함수의 타입은 string으로 자동 정의됨
 */

function func2(value: number) {
    if (value < 10) return value;
    else return `${value} is too big`;
}
/**
 * 반환이 여러 개 있는 함수도 타입 추론 가능
 * func2의 타입은 number | string 으로 정의됨
 * 이렇게 반환이 많은 경우 타입을 따로 입력하지 않아도 자동으로 타입이 추론되기 때문에 편하게 코드 작성 가능
 * 물론 필요한 경우 명시적으로 입력하면 됨!
 */

 

 

타입 가드(Type Guard)

타입 가드는 자동으로 타입의 범위를 좁혀 주는 타입스크립트의 기능이다. 타입가드를 잘 활용하면 "as" 와 같은 타입 단언 코드를 피할 수 있기 때문에 가독성이 좋아진다.

export {};

function print(value: number | string | object) {
    if (typeof value === 'number' || typeof value === 'object') console.log((value as number).toFixed(2));
    else console.log((value as string).trim());
}
/**
 * as? as 앞에 있는 값의 타입을 개발자가 확신하는 경우에 그 타입을 적어줌으로써 타입스크립트에게 오른쪽에 있는 타입을 강제하는 기능
 * 타입스크립트는 number라고 생각하고 있지 않지만 개발자는 number라고 타입스크립트한테 주입하는 것
 * 이 키워드는 정말 어쩔 수 없는 경우에만 사용. as를 많이 사용할수록 버그의 위험도 높아짐
 * 위의 함수를 예로 들면, value의 타입이 number이거나 object일 수 있는데 as를 통해 number로 강제함 --> 버그 위험
 */

function print2(value: number | string) {
    if (typeof value === 'number') console.log((value).toFixed(2));
    else console.log((value).trim());
}
/**
 * 위 함수에서의 typeof는 자바스크립트의 typeof로 타입스크립트의 그것과는 다름
 * 위 함수의 typeof는 오른쪽 변수의 타입을 문자열로 반환하는 기능
 * 타입가드 기능 덕분에 as를 사용하지 않아도 if문의 value가 각각 number, string인 것을 인식하기 때문에 각각의 메소드 사용 가능
 */


// class 타입가드
/**
 * 클래스의 경우 instanceof라는 키워드 사용 가능
 * instanceof는 자바스크립트에도 있는 기능(값의 영역에서 사용하는 기능)
 * 아래 함수 print3의 instanceof를 예시로 보면, value가 Person 클래스에 포함된 속성인지 검사함
 */
class Person {
    name: string;
    age: number;
    constructor(name: string, age: number) { 
        this.name = name;
        this.age = age;
    }
}
class Product {
    name: string;
    price: number;
    constructor(name: string, price: number) {
        this.name = name;
        this.price = price;
    }
}
function print3(value: Person | Product) {
    console.log(value.name);
    if (value instanceof Person) console.log(value.age);
    else console.log(value.price);
}
/**
 * 타입스크립트는 if문에서 value가 Person의 객체라는 것을 알고 else에 있는 value가 Product의 객체라는 것을 앎
 * 그래서 타입가드가 적용되어서 value.age, value.price 사용 가능 
 */


interface Person1 {
    name: string;
    age: number;
}
interface Product1 {
    name: string;
    price: number;

}
function print4(value: Person1 | Product1) {
    console.log(value.name);
    if (value instanceof Person1) console.log(value.age);    // Person1은 형식만 참조, 값으로 사용되지 않기 때문에 에러!
    else console.log(value.price);
}
/**
 * 그러나 위의 경우처럼 인터페이스로 타입을 정의하면 에러가 발생함
 * 인터페이스는 타입을 위한 코드이기 때문에 컴파일 후에는 다 제거되기 때문
 * 그런데 instanceof는 값의 영역에 있기 때문에 실제 컴파일 후에 돌아가는 코드임
 * Person1은 컴파일 후에 존재하지 않기 때문에 에러 발생. instanceof 오른쪽에는 클래스나 생성자함수가 올 수 있음
 */


// discriminated union
interface Person {
    type: 'a';
    name: string;
    age: number;
}
interface Product {
    type: 'b';
    name: string;
    price: number;
}
function print5(value: Person | Product) {
    if (value.type === 'a') console.log(value.age);
    else console.log(value.price);
}
/**
 * 인터페이스를 구별하기 위한 방법: 식별 가능한 유니온 타입을 이용하는 것(영어로 discriminated union이라 불림)
 * 인터페이스에서 식별 가능한 유니온 타입은 같은 이름의 속성을 정의하고 속성의 타입은 모두 겹치지 않게 정의하면 됨
 * 위의 print5 함수를 예로 들면, Person과 Product에는 type이라는 같은 이름의 속성을 정의하고, 각각 a와 b라는 타입으로 겹치지 않게 정의함
 * print5에서 value.type이 'a'이면 Person타입, 아니면 Product 타입
 * as와 같은 타입 단언 코드를 작성하지 않았음에도 Person에만 있는 age라는 속성을 사용하려고 할 때 에러가 나지 않음
 * 왜? 타입가드가 동작하기 때문
 */


// 식별 가능한 유니온 타입은 서로 겹치지 않기 때문에  switch문에서 사용하기 좋음
function print6(value: Person | Product) {
    switch (value.type) {
        case 'a': 
            console.log(value.age);
            break;
        case 'b':
            console.log(value.price);
            break;
    } 
}
/**
 * switch문의 조건으로 타입 속성값을 입력하면 각각의 case에서 타입을 구분해서 처리할 수 있음
 */


// 타입 가드를 활용하는 또다른 방법: 타입을 검사하는 함수를 작성하는 방법
interface Person {
    name: string;
    age: number;
}
interface Product {
    name: string;
    price: number;
}
function isPerson(x: Person | Product): x is Person {
    return (x as Person).age !== undefined;
}
function print7(value: Person | Product) {
    if (isPerson(value)) console.log(value.age);
    else console.log(value.price);
}
/**
 * 위의 인터페이스에는 type 속성이 없고, age와 price 속성의 차이로 구별할 수 있음
 */

//위의 방법보다 속성 이름을 검사하는 방법이 더 간편함
function print8(value: Person | Product) {
    if ('age' in value) console.log(value.age);
    else console.log(value.price);
}
/**
 * 하지만 속성의 수가 많고, 같은 이름이 중복되면 이런 방법은 사용하기 힘들기 때문에 그럴 때는 식별 가능한 유니온 타입을 사용하면 더 좋음
 */

 

 

 

 

 

 

 

 

 

 

 

728x90
LIST

댓글