본문 바로가기
TypeScript

keyof, typeof, as const

by 찬찬2 2022. 9. 28.

 

객체의 속성명(key)을 타입으로 사용하고싶을때...

 

const obj = { a: "123", b: "hello", c: "world" }
type Key = keyof obj // obj는 "값"이기 때문에 타입으로 타이핑할 수 없기 때문에 오류로 표기된다.

const obj = { a: "123", b: "hello", c: "world" }
type Key = keyof typeof obj // obj의 키(key)들을 타입으로써 사용가능하다. type Key = "a" | "b" | "c" 와 같다.

// USE CASES...

// 1
type Movie = {
   id: number;
   name: string;
   lengthInMinutes: number;
}

const titanic: Movie = {
   id: 1,
   name: 'Titanic',
   lengthInMinutes: 269
}

type GetPropertyType<T> = (obj: T, key: keyof T) => T[keyof T]

const getProperty: GetPropertyType<Movie> = (obj, key) => obj[key]

getProperty(titanic, 'name') // Titanic
getProperty(titanic, 'lengthInMinutes') // 269
getProperty(titanic, 'description') // not assignable error


// 2
type Optional<Type> = {
   [Property in keyof Type]+?: Type[Property]
}

type Movie = {
   name: string
   lengthInMinutes: number
   description: string
}

type MutableMovie = Optional<Movie>
type MutableMovie = Partial<Movie>

 

반대로 값(value)을 타입으로 사용하는 방법은

 

const statuses = {
    pending: "PENDING",
    inProgress: "IN_PROGRESS",
    completed: "COMPLETED"
} as const;

type Status = typeof statuses[keyof typeof statuses]; // Type: "PENDING" | "IN_PROGRESS" | "COMPLETED"

 

여기서 "as const" 가 중요한데

 

const status = {
   pending: "PENDING",
   inProgress: "IN_PROGRESS",
   completed: "COMPLETED"
} as const


statuses.pending = 'STANDBY'; // error: Cannot assign to 'pending' because it is a read-only property

// 아래처럼 되었다고 보면 된다. (immutable)
const status = {
   readonly pending: "PENDING",
   readonly inProgress: "IN_PROGRESS",
   readonly completed: "COMPLETED"
} as const

 

객체에 "as const" 를 붙혀주면 그 객체는 "readonly" 의 상태가되어 immutable 된다. 즉, 해당 객체에 있는 프로퍼티에 값을 재할당할 수 없다.

 

"as const" 를 사용한다는 것은 해당 변수의 값이 개발자의 의도가 담긴 어떠한 규칙이 있다는 것을 의미이다.

 

// Type
type Status = {
   pending: "PENDING"
   inProgress: "IN_PROGRESS"
   completed: "COMPLETED"  
}

// Object value
const status = {
   pending: "PENDING",
   inProgress: "IN_PROGRESS",
   completed: "COMPLETED"
} as const

 

하나는 타입을 정의한 것이고, 또 하나는 메모리에 저장될 객체, 즉 값이다.

 

위 두개는 같아보이지만 미세한 차이가 있다.

 

// Using "as const"
const status = {
   pending: "PENDING",
   inProgress: "IN_PROGRESS",
   completed: "COMPLETED"
} as const

type StatusTypeA = typeof status[keyof typeof status]; // "PENDING" | "IN_PROGRESS" | "COMPLETED"
type StatusKeyA = keyof typeof status; // "pending" | "inProgress", "completed", 객체 리터럴의 속성명을 타입으로 만들때 as const 가 없어도된다.

// Two seperate types
type Status = {
   pending: "PENDING"
   inProgress: "IN_PROGRESS"
   completed: "COMPLETED"  
}

type StatusTypeB = Status[keyof Status]; // "PENDING" | "IN_PROGRESS" | "COMPLETED"
type StatusKeyB = keyof Status; // keyof Status

 

StatusTypeB를 VSC에서 hover 했을때 "keyof Status" 로 표기되는 것을 볼 수 있다. 나는 "keyof Status" 가 아닌, 정확히 어떤 값("pending" | "inProgress" | "completed")인지 보여줬으면 했다.

 

왜 이렇게 표현 방식이 다른지 알아보았는데, 타입스크립가 컨텍스트에 따라 타입을 추론하는 방식이 다르기 때문이다.

 

타입스크립트는 StatusTypeA 에 타입을 정의할때 객체 리터럴의 immutable/readonly 된 값에서 추론한 반면, StatusTypeB 는 타입 자체에서 추론했다고 볼 수 있다.

 

즉, 타입스크립트는 객체 리터럴을 구체적으로 보고 추론한 반면 후자는 제너럴하게 추론한 것이다.

 

type AlphabetTypeA = string[]; // 제너럴한 추론
type AlphabetTypeB = ['abc', 'defg', 'hijk']; // 구체적인 추론

const array:AlphabetTypeA = ['abc', 'defg', 'hijk'];

const array:AlphabetTypeB = ['abc', 'defg', 'hijk'];

 

array 는 AlphabetTypeA 와 AlphabetTypeB 를 모두 만족한다.

 

 

댓글