Programming/Typescript

04-생산성을높이는타입스크립트의기능

류시명 2021. 11. 22. 18:44

인프런 이재승 님 타입스크립트 시작하기 강의를 듣고 여러 레퍼런스를 참고하여 정리한 노트입니다.

1. 타입 추론

정적 타입 언어의 단점은

타입을 정의하는데 시간과 노력이 많이 들어서 생산성이 줄어들 수도 있다는 것이다.

따라서 타입 추론을 통해 필요한 경우에만 타입정의를 해야 한다.

let으로 변수 선언 시

let v1 = 123;
let v2 = 'abc';
let v3;
v1 = 'a'; // error
v2 = 456; // error
v3 = 123;
v3 = 'abc';
v3 = false;

위 코드에는 타입 지정을 전혀 하지 않았다.

하지만 v1의 최초 할당값이 123으로 number이므로 자동으로 number로 타입이 지정되었고

같은 이유로 v2는 string으로 지정되었다.

v3는 처음 선언 시 값을 할당하지 않아 타입이 any로 지정되었다.

이처럼 let으로 선언하면 할당값의 타입이 자동으로 지정된다.

const로 변수 선언 시

const v1 = 123;
const v2 = 'abc';
let v3: typeof v1 = 234; // error.
// let v3: typeof v1 = 123; // 가능

반면 const는 좀더 엄격하다. 할당된 값의 타입이 지정되는 것이 아니라 할당된 값이 타입으로 지정된다.

따라서 v1의 타입은 number가 아니라 123이라는 리터럴이 된다.

마찬가지로 v2의 타입은 string이 아니라 아니라 'abc'이라는 리터럴이 된다.

배열과 객체의 경우

배열과 객체의 경우를 보자.

const arr1 = [10, 20, 30]; // number[]
const [n1, n2, n3] = arr1; // 비구조화 할당을 해도 각 변수는 number
arr1.push('a'); // error. number배열이기 때문에

const obj = { id: 'abcd', age: 123, language: 'kor' };
const { id, age, language } = obj; // 각 속성들은 할당된 값의 타입으로 지정이 된다.
console.log(id === age); // error. id와 age의 타입이 다르기 떄문에 비교 불가

arr1과 obj에는 따로 타입을 지정하지 않았지만 자동으로 타입이 지정되고 있다.

심지어 비구조화 할당을 통해 얻은 변수들도 해당 타입을 잘 따르고 있다.

따라서 arr1은 number[]이기 때문에 'a'를 push할 수 없고

id는 string, age는 number이기 때문에 둘을 비교할 수 없다.

인터페이스의 경우

// 부모
interface Person {
    name: string;
    age: number;
}
// 자식1
interface Korean extends Person {
    liveInSeoul: boolean;
}
// 자식2
interface Japanese extends Person {
    liveInTokyo: boolean;
}

const p1: Person = { name: 'simi', age: 30 };
const p2: Korean = { name: 'simi', age: 30, liveInSeoul: true };
const p3: Japanese = { name: 'simi', age: 30, liveInTokyo: false };

const arr1 = [p1, p2, p3]; // Person[].
const arr2 = [p2, p3]; // (Korean | Japanese)[].

arr1은 Korean과 Japanese는 Person에 할당 가능하기 때문에 제거되고 Person만 남는다. Person[]

arr2는 Korean과 Japanese는 할당관계에 있지 않기 때문에 제거되지 않고 유니온으로 묶이게 된다. (Korean | Japanese)[]

함수의 경우

function func1(a = 'abc', b = 10) {
    return `${a} ${b}`; // 반환값의 타입에 따라 string
}
func1(3, 6); // error. 첫번째 인자가 문자열이어야 함
const v1: number = func1('a', 1); // error. number에 할당할 수 없음

function func2(value: number) {
    if (value < 10) {
        return value;
    } else {
        return `${value} is too big`;
    }
    // return 타입은 number | string
}

파라미터에 기본값을 입력하면 해당 값의 타입이 지정된다. 반환값 또한 반환값의 타입으로 자동 지정된다.

반환값의 타입이 여러 개여도 union타입으로 묶여서 잘 지정된다.

하지만 필요에 따라 타입을 명시적으로 지정해줘야 하는 경우가 있다.

예를 들어 func1 함수에서 파라미터 a의 기본값을 'abc'로 유지하면서도 숫자나 문자열 모두 받고 싶다면 아래와 같이 적으면 된다.

function func1(a: number | string = 'abc', b = 10) {
    return `${a} ${b}`;
}
func1(3, 6); // 가능

2. 타입 가드

자동으로 타입의 범위를 좁혀주는 타입스크립트의 기능

타입가드를 잘 사용하면 as와 같은 타입 단언 코드를 피할 수 있기 때문에

생산성과 가독성이 높아진다.

function print(value: number | string)  {
    if (typeof value === 'number') {
        console.log((value as number).toFixed(2));
    } else {
        console.log((value as string).trim());
    }
}

as라는 키워드는 X as T에서 X라는 변수의 타입을 T라고 확신할 수 있을 때, 강제로 X에 T라는 타입을 주입하는 것이다.

이 키워드는 정말정말 어쩔 수 없을 때만 사용해야 하고 사용을 지양해야 한다.

as를 많이 사용할수록 사이드 이펙트가 발생할 확률이 올라가기 때문인데 예를 들어

function print(value: number | string)  {
    if (typeof value === 'number' || typeof value === 'object') {
        console.log((value as number).toFixed(2));
    } else {
        console.log((value as string).trim());
    }
}

로 수정되면 더이상 value를 number라고 단언할 수 없기 때문이다.

이때 여기서 사용된 typeof는 자바스크립트의 것이다.

타입스크립트의 typeof는 타입의 영역에서 사용되는 것이고,

위의 자바스크립트의 typeof는 값의 영역에서 사용되며 해당 값의 type을 문자열로 반환한다.

typeof를 이용한 타입 가드

function print(value: number | string)  {
    if (typeof value === 'number') {
        console.log(value.toFixed(2)); // value의 타입 number
    } else {
        console.log(value.trim()); // value의 타입 string
    }
}

위의 코드를 다시 가져와서 as 키워드를 제거해봐도 잘 작동된다. 심지어 각 조건분기에 맞게 value의 타입이 따로 지정되었다.

이처럼 값의 영역에서 사용한 코드를 분석해서 타입의 범위를 좁혀주는 기능이 타입 가드이다.

instanceof를 이용한 타입 가드

역시 자바스크립트의 키워드이고

O instanceof C 에서 O가 C의 인스턴스인지 검사하여 boolean값을 반환한다.

class Person {
    constructor(public name: string, public age: number) {}
}

class Product {
    constructor(public name: string, public price: number) {}
}

function print(value: Person | Product) {
    console.log(value.name);
    if (value instanceof Person) {
        console.log(value.age); // value의 타입: Person.
    } else {
        console.log(value.price); // value의 타입: Product.
    }
}

인터페이스에서는?

interface Person {
    name: string;
    age: number;
}

interface Product {
    name: string;
    price: number;
}

function print(value: Person | Product) {
    if (value instanceof Person) { // error. 
        console.log(value.age);
    } else {
        console.log(value.price);
    }
}

instanceof 를 사용할 때 주의해야 할 점은, 이 키워드 오른쪽에는 클래스나 생성자 함수에 사용해야 한다는 것이다.

interface는 타입을 위해서 존재하는 것으로 컴파일 후 사라지는 코드이기 때문에 사용할 수 없다.

interface에서 타입 가드를 사용하려면 식별가능한 유니온 타입을 사용해야 한다.

식별가능한유니온타입(discriminated union)

interface에서 식별가능한 유니온 타입이란

key의 이름은 동일하고, 그 value는 모두 달라서 특정할 수 있는 속성을 말한다.

// discriminated union
// 식별 가능한 유니온 타입
interface Person {
    type: 'a',
    name: string;
    age: number;
}

interface Product {
    type: 'b',
    name: string;
    price: number;
}

function print(value: Person | Product) {
    // 타입가드 잘 동작.
    if (value.type === 'a') {
        console.log(value.age); // value의 타입: Person
    } else {
        console.log(value.price); // value의 타입: Product
    }
}

위 코드에서 타입가드는 잘 작동하고 있다.

이와 같은 식별가능유니온타입을 사용할 땐 switch문을 사용하면 좋다.

interface Person {
    type: 'a',
    name: string;
    age: number;
}

interface Product {
    type: 'b',
    name: string;
    price: number;
}

interface Product2 {
    type: 'c',
    name: string;
    price2: number;
}

function print(value: Person | Product | Product2) {
    switch (value.type) {
        case 'a':
            console.log(value.age);
            break;
        case 'b':
            console.log(value.price);
            break;
        case 'c':
            console.log(value.price2);
            break;
    }
}

in 사용

타입 가드를 활용하는 다른 방법으로는 타입 검사 작성이 있다.

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 print(value: Person | Product) {
    if (isPerson(value)) {
        console.log(value.age);
    } else {
        console.log(value.price);
    }
}

위 코드에서 Person과 Product를 구별하기 위해, Person에는 age가 있고, Product에는 price가 있는 걸 활용해보기로 한다.

따라서 isPerson 함수에서는 age라는 프로퍼티가 있는지 없는지 검사하게 된다.

하지만 위의 코드는 as를 사용했으며 번거롭다.

자바스크립트의 in 키워드를 사용하면 편하게 작성할 수 있다.

자바스크립트의 in 키워드는 X in O 일 때, X가 O라는 객체에 속하는 속성인지 검사하여 그 값을 boolean으로 반환한다.

interface Person {
    name: string;
    age: number;
}

interface Product {
    name: string;
    price: number;
}

function print(value: Person | Product) {
    if ('age' in value) {
        console.log(value.age); // value의 타입: Person
    } else {
        console.log(value.price); // value의 타입: Product
    }
}

이 방법이 식별가능한유니온타입을 이용하는 방법보다 간단하지만,

속성의 수가 많이지면 사용하기 어렵다. 즉, 상황에 맞춰 사용하면 된다.

참고

반응형