Menu

TypeScript 느낌표(!) 연산자: non-null 단언

user!처럼 값 뒤에 붙은 느낌표는 non-null 단언 연산자입니다. 런타임 검사 없이 타입에서 null과 undefined를 없앱니다. x!가 하는 일, 확정 할당 형태인 let x!: T와 prop!: T, 이것이 위험한 이유, 더 안전한 대안을 알아봅니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

표현식 뒤의 느낌표 value!는 non-null 단언 연산자입니다. 타입에서 null과 undefined를 없애므로 number | undefined를 number처럼 쓸 수 있습니다. 이것은 컴파일러에 대한 약속이지 검사가 아닙니다. 런타임에는 아무 일도 일어나지 않습니다.

!가 없으면 tea.toFixed(2)는 오류 TS18048, 'tea' is possibly 'undefined'입니다. !가 있으면 키가 존재한다고 컴파일러에 알렸으므로 코드가 컴파일됩니다.

런타임에 x!가 하는 일: 없음

!는 출력에서 지워집니다. prices.get("coffee")!의 컴파일된 JavaScript는 그냥 prices.get("coffee")입니다. 단언이 틀렸다면 오류는 나중에, 빠진 값이 처음 쓰이는 곳에서 나타납니다.

프로그램은 Cannot read properties of undefined (reading 'toFixed')를 출력합니다. ! 다음 줄에서 멈추는데, 실제 코드에서는 훨씬 멀리 떨어질 수 있습니다. undefined가 객체에 저장되었다가 다른 파일에서 터질 수도 있습니다. 이 거리 때문에 !가 위험합니다.

확정 할당: let x!: T

선언에서 같은 기호는 관련된 다른 뜻을 갖습니다. TypeScript는 변수가 읽히기 전에 대입되는지 추적하지만, 다른 함수 안에서 한 대입은 따라가지 못합니다.

index.ts(9,13): error TS2454: Variable 'config' is used before being assigned.

let config!: { port: number };는 확정 할당 단언으로, "읽기 전에 반드시 대입된다"는 뜻입니다. 오류는 해결되지만 x!와 같은 함정이 있습니다. init()이 한 번이라도 건너뛰어지면 읽을 때 undefined를 얻습니다. 대개는 구조를 바꾸는 편이 낫습니다. 예를 들어 init이 객체를 반환하게 하고 const config = init();로 쓰세요.

클래스 속성: prop!: T

strictPropertyInitialization(strict에 포함)이 켜져 있으면 모든 클래스 속성은 선언이나 생성자에서 초기화되어야 합니다. 그렇지 않으면 TS2564를 받습니다: Property 'socket' has no initializer and is not definitely assigned in the constructor. 속성이 나중에 메서드나 프레임워크에 의해 설정된다면, prop!: T로 컴파일러가 받아들이게 할 수 있습니다.

첫 번째 console.log가 빈틈을 보여 줍니다. 타입은 socket이 항상 있다고 하지만, open() 전에는 undefined입니다. 그 시점에 c.socket.send를 호출하면 컴파일은 되고 실행하면 예외가 납니다. 속성이 정말로 없을 수 있다면 socket?: ...로 선언하고 확인하거나, 생성자에서 객체를 만드세요. prop!가 표준 관행인 주된 곳은 생성 이후에 속성을 채우는 프레임워크입니다. Angular의 @ViewChild(...) child!: ChildDirective(ngAfterViewInit이 실행되기 전에 설정됨)나, 행을 불러올 때 라이브러리가 열을 채우는 ORM 엔티티 클래스(MikroORM 문서는 @Property() title!: string이라고 씁니다)가 그렇습니다.

더 안전한 대안

대부분의 !는 컴파일러가 검사하는 코드로, 또는 올바른 자리에서 분명하게 실패하는 검사로 바꿀 수 있습니다.

두 헬퍼 모두 !처럼 타입을 좁히지만, 가정이 틀리면 다른 어딘가의 TypeError 대신 그 자리에서 missing HOST를 냅니다. assertDefined는 단언 함수(asserts value is ...)이므로, 호출 뒤에 컴파일러는 host를 string으로 취급합니다. 더 많은 패턴은 타입 가드에 있습니다.

이것 대신이렇게 쓰기값이 없을 때 일어나는 일
user!.nameif (user) { user.name }블록을 건너뜀
user!.nameuser?.nameundefined
count!count ?? 0기본값을 씀
map.get(k)!must(map.get(k), "k")그 줄에서 명확한 오류
let x!: Tconst x = compute()잘못될 일이 없음

다른 느낌표들

!는 위치에 따라 다른 뜻을 갖습니다.

코드의미
value!(표현식 뒤)non-null 단언, TypeScript 전용
let x!: T, prop!: T확정 할당 단언, TypeScript 전용
!value(표현식 앞)논리 NOT, 평범한 JavaScript
!!value불리언으로 변환, 평범한 JavaScript
a !== b, a != b부등 비교, 평범한 JavaScript

컴파일 시점에 지워지는 것은 처음 두 가지뿐입니다. !value와 !!value는 런타임에 실행되어 불리언을 반환합니다.

자주 묻는 질문

TypeScript에서 변수 뒤의 느낌표는 무슨 뜻인가요?

value!는 non-null 단언 연산자입니다. value가 null이나 undefined가 아니라고 컴파일러에 알리므로, 타입에서 그 두 멤버가 빠집니다. string | undefined가 string이 됩니다. 출력된 JavaScript에서는 제거되고 런타임 검사를 더하지 않으므로, 틀렸다면 프로그램은 나중에 TypeError로 실패합니다.

TypeScript에서 !와 ?의 차이는 무엇인가요?

x!는 값이 있다고 단언하고 검사 없이 null이 아닌 타입을 줍니다. x?.y는 런타임에 확인합니다. x가 null이나 undefined면 멈추고 undefined를 반환합니다. 선언에서 name?: string은 속성을 선택적으로 만들고, name!: string은 필수 속성이 컴파일러가 볼 수 없는 어딘가에서 대입된다고 말합니다.

let x!: string은 무슨 뜻인가요?

확정 할당 단언(definite assignment assertion)입니다. 컴파일러가 증명할 수 없더라도(예를 들어 대입이 다른 함수 안에서 일어나는 경우) 변수를 읽기 전에 대입된다고 컴파일러에 알립니다. 이것이 없으면 변수를 읽는 것은 오류 TS2454, Variable 'x' is used before being assigned입니다.

"has no initializer and is not definitely assigned in the constructor"는 어떻게 고치나요?

strictPropertyInitialization에서 나오는 오류 TS2564입니다. 속성에 초기값을 주거나, 생성자에서 대입하거나, 선택적으로 만들거나(prop?: T), 프레임워크나 초기화 메서드가 정말로 사용 전에 값을 설정한다면 prop!: T라고 쓰세요. 아무것도 이 약속을 검사하지 않으므로 !는 마지막 선택지입니다.

non-null 단언 연산자는 나쁜 습관인가요?

틀린 것은 아니지만 ! 하나하나가 검사되지 않는 주장입니다. @typescript-eslint/no-non-null-assertion 같은 린트 설정이 이를 표시합니다. 좁히는 검사(if (x), x ?? fallback, x?.y)나 명확한 오류를 던지는 헬퍼를 쓰세요. !는 컴파일러가 따라갈 수 없는 논리로 값이 보장되는 곳에만 남겨 두세요.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기