as const란?

조민호·2023년 10월 27일
0


JS에서 let과 const 의 가장 큰 차이점은 재할당 여부이다

그렇지만 TS에서는 또 하나의 차이점이 존재한다

바로 변수를 선언할 때 타입 추론이 다르게 일어난다는 것이다

let a = 'hi'; // let a: string
const b = 'hi'; // const b: "hi"

let a = 1; // let a: number
const b = 1; // const b: 1
  • let으로 선언한 변수들은 그에 맞는 타입이 되지만
  • const로 선언한 변수들은 리터럴 타입이 되는 것이다


그렇지만 배열,튜플,객체의 경우에는 let 과 const 모두

리터럴 타입으로 범위를 좁히지 않는다

let a = [1, 2, 3, 4] // let a: number[]

let b = [1, 'hi'] // let b: (string | number)[]

let c = {
  num: 1,
  str: 'hi',
}

// let c: {
//   num: number;
//   str: string;
// }
const a = [1, 2, 3, 4] // const a: number[]

const b = [1, 'hi']; // const b: (string | number)[]

const c = {
  num: 1,
  str: 'hi'
};

// const c: {
//   num: number;
//   str: string;
// }

TypeScript 3.4 버전부터 도입된 const assertion을 활용하여 이런 경우에도 타입 추론의 범위를 좁힐 수 있다

const assertion은 말 그대로 상수라고 주장하는 것인데, 원래 상수가 아닌 것을 상수인 것으로 선언하는 기능이라고 유추 할 수 있다

  • 사용법은 변수 선언문 뒤에 as const를 추가하거나
  • 제네릭 부분에 <const>를 사용하면 된다

as const를 사용하게 되면 let으로 선언한 일반 변수에도 as const를 붙여서 동일하게 const assertion을 적용할 수 있다

let a = 'hi' as const; // let a: "hi"
let b = 1 as const; // let b: 1

이처럼 let에서 as const를 사용하게 되면 리터럴 타입이 적용되는 것을 볼 수 있다

그리고 타입이 리터럴 타입으로 고정되었기 때문에 다른 값으로 재할당이 불가능하다

그렇지만 이건 지양되는 방법이다

애초에 처음부터 let 대신 const를 사용하면 되기 때문이다


그러면 언제 as const를 사용하는 것일까?

as const 는 Object 타입들에 대해 타입을 좁혀서 상수 형태로 사용하고자 할 때 사용한다

배열 , 튜플 , 객체의 경우 let 과 const 둘 다 리터럴 타입이 아닌
일반 타입으로 정의가 된다

let a = [1, 2, 3, 4]  // let a: number[]
const a = [1, 2, 3, 4]  // const a: number[]

그러므로 이런 Object 타입에다가 as const를 사용하게 되면
상수 형태로 값이 적용된다

또한 이렇게 as const로 객체의 각 키값에 타입을 리터럴 타입으로 고정함으로써 readonly가 자동으로 적용된다

let a = [1, 2, 3, 4] as const // let a: readonly [1, 2, 3, 4]

let b = [1, 'hi'] as const // let b: readonly [1, "hi"]

let c = {
    num: 1,
    str: 'hi',
} as const
//   let c: {
//     readonly num: 1
//     readonly str: 'hi'
//   }
const a = [1, 2, 3, 4] as const // const a: readonly [1, 2, 3, 4]

const b = [1, 'hi'] as const // const a: readonly [1, "hi"]
  // const a = <const>[1, 'hi'];와 동일

const c = {
    num: 1,
    str: 'hi',
} as const

// const b: {
//   readonly num: 1;
//   readonly str: "hi";
// }

const assertion은 문자열이나 숫자, 배열이나 객체 리터럴 외에도

enum members, boolean에도 적용할 수 있다.


제네릭 에다가 사용할 경우

일반적으로 제네릭에다가 리터럴 타입을 넣을 때는

  • 리터럴 타입 자체를 넣어준다
    	type arr = ['a', 'b', 'c'];  // type alias
    
      type First<T extends string[]> = T[0];
    
      type head1 = First<arr>; // expected to be 'a'
    
      let a: head1 = 'a'; // OK
      let b: head1 = 'b'; // error
  • 변수를 넣어 버리면 리터럴 타입이 아니게 된다
    	let arr = ['a', 'b', 'c']; // 일반 변수
    
      type First<T extends string[]> = T[0]; // string[]의 [0]번째 타입이므로 string타입
    
    	// typeof arr은 리터럴 타입이 아닌 string[]타입이 됨
      type head1 = First<typeof arr>; // expected to be string
    
      let a: First<typeof arr> = 'a'; // OK
      let b: First<typeof arr> = 'b'; // OK , 리터럴 타입이 아니므로 이것도 통과가 돼버림
  • 이럴 때 , 일반 변수 또한 리터럴 타입으로 쓸 때 as const를 사용하면 된다
    	let arr = ['a', 'b', 'c'] as const;
    
      //	['a', 'b', 'c']의 [0]이므로 'a' 리터럴 타입이 반환 됨
      type First<T extends readonly string[]> = T[0];
    
    	// typeof arr은 ['a', 'b', 'c'] 리터럴 타입
      type head1 = First<typeof arr>; // expected to be 'a'
    
    	let a: head1 = 'a'; // OK
      let b: head1 = 'b'; // error

이렇게 되는 이유는 TS는 타입이 런타임이 아닌 컴파일 타임에 결정이 된다

  1. 그러므로 type arr = ['a', 'b', 'c'] 같은 경우는 당연히 타입으로 지정한 거니까 컴파일 타임에도

    이걸 리터럴로 읽어 낼 수 있지만

  2. 일반 변수를 그대로 사용했을 경우에는 그 값을 컴파일 타임에 읽지 못한다

    그러므로 그 안에 값에 뭐가 들었는지 알 수 없기에

    아래와 같이 사용하면 에러가 발한다

    type head1 = First<arr>; // error

    제네릭의 <>에는 타입을 넣어줘야 하는데 arr의 값이 뭐가 들었는지 컴파일 타임에는 모르기 때문이다. 그러므로 사용한 것이 typeof 키워드인데

    type head1 = First<typeof arr>; 

    이걸 사용하면 컴파일 타임에 해당 변수의 타입을 읽어 올 수 있지만

    이 역시 우리가 원했던 리터럴 ['a', 'b', 'c'] 가 아닌 string[] 형태로만 읽어와진다

  3. 이때 as const를 통해 상수화를 시켜두면

    컴파일 타임에도 그 값을 읽어와서 리터럴 타입으로 사용할 수 있는 것이다

    	let arr = ['a', 'b', 'c'] as const;
    
    	// typeof arr은 ['a', 'b', 'c'] 리터럴 타입
      type head1 = First<typeof arr>; 

    일반적으로 배열 또는 객체를 선언하면 TypeScript는 그 내용을 변경 가능하다고 가정하고 그에 맞는 타입을 추론한다

    따라서, 배열이나 객체의 타입은 그 요소의 타입을 보다 덜 정확하게 나타내게 된다

    하지만 as const를 사용하면 TypeScript는 해당 배열이나 객체의 내용이 변경되지 않을 것이라고

    가정하고, 그에 따라 더 정확한 타입을 추론할 수 있는 것이다

    즉, as const는 TypeScript에게 "이 변수는 변경되지 않습니다. 그래서 이 변수의 값은 항상 이것일 것입니다."라고 알려주는 역할을 한다. 그래서 as const가 적용된 변수의 값을 컴파일 타임에서 알 수 있게 되고 ,

    배열이나 객체의 요소 각각에 대해 리터럴 타입을 추론하는 것을 가능하게 한다

    따라서 as const를 사용하면 컴파일 타임에도 해당 배열이나 객체의 정확한 내용(값과 타입 모두)을 알 수 있다

    이로 인해 리터럴 타입을 제네릭에 사용할 수 있게 되는 것이다



또한 , typeof를 제네릭에 사용할 때 만약 해당 타입이 readonly라면

반드시 제네릭에도 readonly를 작성해 줘야 한다

  • 일반 리터럴 타입 (=수정이 가능)
    type First<T extends any[]> = T[0];
    type arr1 = ['a', 'b', 'c'];
    type head1 = First<arr1>; // expected to be 'a'
  • 타입에 readonly가 추가된 경우
    type First<T extends **readonly** any[]> = T[0];
    const arr = ['a', 'b', 'c'] as const; 
    type head1 = First<typeof arr>; // expected to be 'a'
    일반 변수에다가 as const를 사용함으로써 리터럴 타입 + 상수화로 인해 readonly가 된다 그러므로 이걸 제네릭에 넣어줄 대는 typeof 와 함께 넣어주는데 이때 , 제네릭에서도 반드시 readonly로 받아줘야 한다



삼항 연산자를 사용한 경우에는 const assertion을 적용할 수 없다

let boolean = true;
let a = (boolean ? 'yes' : 'no') as const
// 'const' 어설션은 열거형 멤버나 문자열, 숫자, 부울, 배열 또는 개체 리터럴에 대한 참조에만 적용할 수 있습니다.ts(1355)

이러한 경우에는 삼항 연산자의 두 선택문 모두에 as const를 사용하여 타입을 좁힐 수 있다

let boolean = true;
let a = boolean ? 'yes' as const : 'no' as const; // let a: "yes" | "no"


참고 :

[TS] const assertion (as const)

Type Challenge - Tuple to Object

profile
웰시코기발바닥

0개의 댓글