📚 타입 단언
타입 단언은 개발자가 직접 타입을 명시해 해당 타입으로 강제하는 방식.
타입스크립트를 컴파일 할 때 타입 검사를 하지 않고, 데이터의 구조도 신경쓰지 않는다.
컴파일러가 가진 정보를 무시하고 개발자가 원하는 임의의 타입을 값에 할당하고 싶을 때 사용한다.
📌 타입 단언은 왜 사용할까?
일례로 변수를 선언할 때 빈 객체에 인터페이스를 정의하면 다음과 같이 에러가 난다.
해당 객체가 Person 인터페이스와 맞게 초기화되지 않았기 때문이다.
타입 에러를 해결하는 방법으론 객체 선언 시점에 속성을 정의하면 된다.
하지만 이미 운영 중인 서비스의 코드나 누군가 만들어 놓은 코드라면 건드리기 까다로울 것이다.
이럴 때 타입 단언을 이용해서 기존 코드를 변경하지 않고도 타입 에러를 해결 할 수 있다.
이처럼 타입 단언을 이용하면 타입스크립트 컴파일러가 알기 어려운 타입에 대해 힌트를 제공할 수 있다.
또, 선언하는 시점에 속성을 정의하지 않아도 추후에 추가할 수 있는 유연함을 갖게 된다.
📌 타입 단언 문법
Value as Type
을 문법을 사용해 값 Value
를 Type
타입으로 단언할 수 있다.
Value의 타입은 개발자인 내가 더 잘 알아 그러니 Value를 Type 타입 값이라고 생각하고 진행해.
interface Cat {
legs: 4
bark(): void
}
interface Insect {
legs: number
creepy: boolean
}
interface Fish {
swim(): void
}
type Animal = Cat | Insect | Fish
function doSomethingWithAnimal(animal: Animal) {
;(animal as Fish).swim()
}
animal
의 타입을 Fish
로 단언하고 있다.
원래라면 Cat
과 Insect
타입에는 swim
메소드가 없다는 에러가 발생했겠지만,
타입 단언으로 인해 컴파일러는 animal
의 타입을 Fish
로 해석하고, 에러 없이 컴파일 된다.
📌 any 타입 단언
any
타입으로 단언함으로써 특정 값에 대한 타입 검사를 사실상 완전히 무효화할 수 있다.
(3 as any).charAt(1) // Uncaught TypeError: 3.charAt is not a function
위 코드는 런타임 에러를 발생시키지만 타임검사는 통과한다. 때문에 any
를 사용한 타입단언은 가급적 지양해야한다.
타입스크립트를 근본적으로 사용하는 이유는 런타임에 발생할 에러를 컴파일 타임에 방지하기 위해서인데, any
를 사용한 타입 단언은 그 의도에 정확히 반하기 때문이다.
📌 다중 타입 단언
타입 단언은 여러번 중첩해서 사용할 수 있다.
다중 단언은 호환되지 않는 것이 명백한 타입으로의 단언을 가능케 한다.
아래와 같은 타입 단언에선 타입 에러가 발생한다. Cat
타입을 Insect
로 취급할 수 없다는 것을 컴파일러가 알기 때문이다.
interface Cat {
legs: 4
meow(): void
}
interface Insect {
legs: number
creepy: boolean
}
const cat: Cat = {
legs: 4,
meow() {
console.log('meow')
},
}
const insect: Insect = cat as Insect
// error TS2352: Type 'Cat' cannot be converted to type 'Insect'.
// Property 'creepy' is missing in type 'Cat'.
하지만 이러한 제약은 any
로 한 번 타입 단언을 한 뒤, 그 값을 다시 Insect
로 단언함으로서 피해갈 수 있다. 일단 any
타입으로 취급된 그 값은 모든 타입에 할당 가능하기 때문이다.
const insect: Insect = (cat as any) as Insect // ok
하지만 타입 단언이 막아주는 건 타입 에러 뿐이다. 절대 런타임 에러가 발생하지 않는다는 확신이 있지 않은 한 호환이 불가능한 타입에 any를 사용해 단언하는 일은 피하는 것이 좋다.
📌 타입 단언(as Type) 보다 타입 선언(: Type) 사용하기
1. 타입 단언은 타입 체크를 하지 않는다.
타입 선언은 할당되는 값이 해당 인터페이스를 만족하는 검사하는데, 타입 단언은 강제로 타입을 지정했으니 타입 체커에게 오류를 무시하라고 하는 것.
interface Person {
name: string
}
const alice: Person = {}; // 'Person' 유형에 필요한 'name' 속성이 '{}' 유형에 없습니다.
const bob = {} as Person; // 오류 없음
2. 없는 속성에 접근해도 컴파일 시 에러가 발생하지 않는다.
const alice: Person = {
name: 'Alice',
occupation: 'TypeScript Developer'
// ~~~~~~~ 개체 리터럴은 알려진 속성만 지정할 수 있음, Person 형식에 occupation이 없습니다.
}
const bob = {
name: 'Bob',
occupation: 'Javascript Developer'
} as Person // 오류 없음
타입 단언을 남용하면 런타임 에러에 취약해질 수 있다. 가급적 타입 단언보다는 타입 추론과 타입 선언을 사용하는 것이 좋다.
📌 Non-null 단언 연산자 (non null assertion)
!
연산자(단언 연산자)는 피연산자가 null
또는 undefined
가 아님을 단언할 때 사용한다.
function greet(name: string | null) {
// name이 null 또는 undefined일 때 에러가 발생하지 않도록 ! 연산자를 사용
console.log(`Hello, ${name!.toUpperCase()}`);
}
greet("Alice"); // 안전한 호출
greet(null); // TypeScript는 경고하지만 컴파일 가능하며 런타임에서 에러 발생
TypeScript ESLint에서는 Non-null assertion
을 사용하면 The strict null-checking mode의 이점이 없어지므로 사용을 권장하지 않는다
Reference
https://ahnheejong.gitbook.io/ts-for-jsdev/06-type-system-deepdive/type-assertion
https://baek.dev/til/typescript/effective-typescript/ch02/item9