TypeScript - 타입 추론 / 단언 / 좁히기

Minkyu Shin·2023년 6월 16일
0

TypeScript

목록 보기
7/7
post-thumbnail

TypeScript

본 내용은 한 입 크기로 잘라먹는 타입스크립트 강의를 바탕으로 작성되었습니다. 사진 자료의 출처 또한 위 강의입니다.

타입 추론

타입 추론이란 타입이 정의되어 있지 않은 변수의 타입을 자동으로 추론하는 타입스크립트의 기능을 말한다. 타입 추론 덕분에 모든 변수에 일일이 타입을 정의하지 않아도 되는 편리함을 준다.

// number 타입으로 자동 추론
let a = 10;

단, 함수의 매개변수 타입과 같이 특정 상황에는 타입을 추론할 수 없다. 이럴 경우 암시적으로 any 타입으로 추론되고 만약 tsconfig.jsonstrict 옵션이 켜져 있다면 이 또한 오류로 판단하게 된다. 그렇다면 어떤 경우에 타입 추론이 가능할까?

타입 추론이 가능한 상황들

1. 변수 선언

변수 선언 시 초기값을 할당하면 해당 값을 기준으로 타입이 추론된다. 만약 초기값을 할당하지 않으면 any 타입으로 추론되므로 주의하자.

// number 타입으로 추론
let a = 10;

// string 타입으로 추론
let b = "hello";

// id, name, profile, urls 프로퍼티가 있는 객체 타입으로 추론
let c = {
  id: 1,
  name: "홍길동",
  profile: {
    nickname: "Gildong",
  },
  urls: ["https://gildong.com"],
};

2. 구조 분해 할당

객체와 배열을 구조 분해 할당하면 각 변수의 타입이 잘 추론된다.

let { id, name, profile } = c;

let [one, two, three] = [1, "hello", true];

3. 함수의 반환값

함수 반환값의 타입은 return 문을 기준으로 추론된다.

function sayHello() { // 반환값이 string 타입으로 추론
  return "hello";
}

4. 기본값이 설정된 매개변수

앞서 함수의 매개변수는 타입을 추론할 수 없어 암시적 any 타입이 된다고 했다. 하지만 매개변수에 기본값이 설정되어 있을 경우에는 예외로 기본값을 기준으로 타입이 잘 추론된다.

// 매개변수가 string 타입으로 추론
function sayHello(message = "hello") {
  return "hello";
}

주의해야 할 상황들

1. 암시적인 any 타입 추론

앞서 변수를 선언하며 초기값을 할당하지 않으면 any 타입으로 추론됨을 살펴보았다. 이렇게 일반 변수의 타입이 암시적 any 타입으로 추론되는 상황은 오류로 판단되지 않는다.

any 타입이었던 변수에 값을 할당하면 그 다음 라인부터 any 타입이 할당된 값의 타입으로 변화하게 된다. 그리고 코드의 흐름에 따라, 즉 변수에 새로 할당된 값이 어떤 타입인지에 따라 타입이 계속 변화한다. 이를 any의 진화라고 표현한다고 한다.

// 암시적 any 타입
let d;

d = 10; // 아래 라인부터 number 타입
d.toFixed();

d = "hello"; // 아래 라인부터 string 타입
d.toUpperCase();
d.toFixed(); // Error

2. const 상수의 추론

const 키워드를 통해 선언된 상수도 타입 추론이 진행된다. const 키워드로 선언된 상수는 초기화 때 설정한 값을 바꿀 수 없기 때문에 특별히 가장 좁은 타입으로 추론된다.

// 10 Number Literal 타입으로 추론
const num = 10;

// "hello" String Literal 타입으로 추론
const str = "hello";

최적 공통 타입 (Best Common Type)

다양한 타입의 요소를 담은 배열을 변수의 초기값으로 설정하게 되면 최적의 공통 타입으로 추론된다.

// (string | number)[] 타입으로 추론
let arr = [1, "string"];

타입 단언

타입 단언이란 컴파일러에게 특정 타입 정보의 사용을 강제하는 것을 말한다. value as Type 문법을 활용하여 값 valueType 타입으로 단언할 수 있다.

다음과 같은 코드를 살펴보자.

type Person = {
  name: string;
  age: number;
};

let person: Person = {}; // Error
person.name = "";
person.age = 20;

Person이라는 타입을 변수 person에 정의해 주고 싶고, 변수를 초기화 할 때는 빈 객체를 할당하고자 하는 상황이다. 타입스크립트를 사용하면 빈 객체가 Person 타입이 아니므로 이 경우는 허용되지 않는다. 이럴 때 사용하는 것이 타입 단언이다.

변수 선언 코드를 다음과 같이 변경해 보자.

// ~~
let person = {} as Person;
// ~~

타입 단언을 해주면 컴파일러에게 마치 ' person 이 Person 타입이 될 것이라는 것을 내가 책임질테니, 네가 갖고 있는 정보는 다 무시하고 person 을 Person 타입의 값이라 생각하고 진행해' 라고 말하는 것과 같다.

타입 단언은 초과 프로퍼티 검사를 피할 때도 활용할 수 있다.

type Dog = {
  name: string;
  color: string;
};

let dog: Dog = {
  name: "멍멍이",
  color: "brown",
  breed: "진도",
} as Dog

단, 타입 단언은 컴파일 타임에 잡을 수 있는 에러를 없앰으로써 원래 발생하지 않을 수 있는 런타임 에러를 발생시킬 수 있으므로 주의해야 한다.

타입 단언의 조건

타입 단언에도 조건이 있는데, value as Type 으로 표현된 단언식에서 valueType 의 슈퍼타입이거나 서브타입이어야 한다.

다중 단언

타입 단언은 여러번 겹쳐 사용할 수 있다.

const num = 10 as unknown as string;

위와 같은 단언의 경우 왼쪽에서 오른쪽으로 단언이 이루어진다. 타입 정의의 순서가

  1. number 타입의 값을 unknown 타입으로 단언
  2. unknown 타입의 값을 string 타입으로 단언

unknown 타입의 값은 모든 타입의 슈퍼타입이기 때문에 그 어떤 타입으로도 단언할 수 있다. 이런 식으로 다중 단언을 사용하면 number 타입의 값이 string 타입으로 정의되는 말도 안되는 상황이 발생하고 오류가 발생할 확률이 매우 높아진다.

타입 단언은 타입 에러만 막아줄 뿐이다. 따라서 절대 런타임 오류가 나지 않을 것이라는 확신이 있거나 런타임 에러가 나도 상관없는 상황이 아니라면 호환되지 않는 타입을 any 또는 unknown 타입을 거쳐 단언하는 일은 피해야 한다.

const 단언

const 타입은 타입 단언 때에만 사용할 수 있는 특수한 타입이다. 값을 const 타입으로 단언하면 마치 변수를 const로 선언한 것과 비슷하게 타입이 변경된다.

// 10 number literal 타입으로 단언
let num = 10 as const;

// 모든 프로퍼티가 readonly를 갖도록 단언
let dog = {
  name: "멍멍이",
  color: "brown",
} as const;

Non Null 단언

Non Null 단언은 값이 undefined 이거나 null 이 아닐 것으로 단언해 주는 것이다. 기본 단언 문법과는 다르게 값 뒤에 느낌표 (!) 를 붙여주는 문법을 사용한다.

type Post = {
  title: string;
  author?: string;
};

let post: Post = {
  title: "게시글1",
};

const len: number = post.author!.length;

Non Null 단언으로 typescript type checker에서 오류를 발생시키는 것을 막을 수는 있지만, 실제로 undefinednull 값이 들어가지 않을 것을 확실할 수 없는 경우가 많다. 이럴 때는 타입 가드를 사용하거나 null 병합 연산자를 사용하는 등 다른 방법을 사용하는 편이 나을 수 있다.

타입 좁히기

보다 넓은(더 많은 경우의 수를 갖는) 타입을 더 좁은(더 적은 경우의 수를 갖는) 타입으로 재정의하는 행위를 타입 좁히기라고 한다. 다음 함수를 예시로 타입 좁히기를 알아보자.

function func(value: number | string) {
  value.toFixed(); // Error
  // or
  value.toUpperCase(); // Error
}

위 함수의 매개변수 value 는 number와 string의 유니온 타입이다. 이 때 number 타입 또는 string 타입에 사용할 수 있는 메서드를 사용하려 하면 오류가 발생하게 된다. value 값의 타입이 해당 메서드를 사용할 수 있을지 여부가 보장되지 않기 때문이다. 이럴 때 타입 좁히기를 사용해 주어 value 의 타입이 메서드를 사용할 수 있는 타입임을 보장해 줘야 한다.

function func(value: number | string) {
  if (typeof value === "number") {
    value.toFixed();
  } else if (typeof value === "string") {
    value.toUpperCase();
  }
}

조건문을 이용하여 변수가 특정 타입임을 보장해주면, 조건문 내부에서는 변수의 타입이 해당 타입으로 좁혀진다. 이에 따라 첫 조건문 내부에서는 value 가 number 타입이 되고, 두번째 조건문 내부에서는 value 가 string 타입이 된다.

조건문과 함께 사용해 타입을 좁히는 표현들을 '타입 가드' 라고 부른다. 타입 가드를 이용하면 타입을 좁혀 사용할 수 있다.

instanceof 타입가드

instanceof를 이용하여 내장 클래스 타입 또는 직접 만든 클래스의 타입을 보장할 수 있는 타입 가드를 만들 수 있다.

function func(value: number | string | Date | null) {
  if (typeof value === "number") {
    console.log(value.toFixed());
  } else if (typeof value === "string") {
    console.log(value.toUpperCase());
  } else if (value instanceof Date) {
    console.log(value.getTime());
  }
}

단, instanceof 연산은 직접 만든 타입과 함께 사용할 수는 없다.

in 타입 가드

직접 만든 타입과 함께 사용하려면 in 연산자를 사용해야 한다.

type Person = {
  name: string;
  age: number;
};

function func(value: number | string | Date | null | Person) {
  if (typeof value === "number") {
    console.log(value.toFixed());
  } else if (typeof value === "string") {
    console.log(value.toUpperCase());
  } else if (value instanceof Date) {
    console.log(value.getTime());
  } else if (value && "age" in value) {
    console.log(`${value.name}${value.age}살 입니다`)
  }
}
profile
개발자를 지망하는 경영학도

0개의 댓글