본문 바로가기

IT

Effective Typescript - 42 - 모르는 타입의 값에는 any 대신 unknown을 사용하기

unknown에는 함수의 반환 값과 관련된 형태, 변수 선언과 관련된 형태, 단언문과 관련된 형태가 있습니다.

 

먼저, 함수의 반환값과 관련된 unknown을 알아보겠습니다.

YAML 파서인 parseYAML 함수를 작성한다고 가정하고 아래의 예제를 살펴봅시다.

// 아이템 38에서 설명한 함수의 반환시 any 지양하기 철학에 위반.
function parseYAML(yaml: string): any {
    // ...
}

const book = parseYAML(`
    name: asdfsafd
    author: abbbb
`);

alert(book.title); // 타입 체킹에서는 이상 없음. 런타임에 undefined 관련 오류가 발생할 수 있음.
book(); // 오류가 없음. 런타임에 함수가 아니라는 오류가 발생할 수 있음.

위 예제의 주석에서 설명한 오류를 타입 채킹 단계에서 좀 더 안전하게 하기 위해서는 parseYAML 함수의 반환 값을 unknown으로 하는 것이 더 낫습니다.

// 아이템 38에서 설명한 함수의 반환시 any 지양하기 철학에 위반.
function parseYAML(yaml: string): any {
    // ...
}

// 함수의 반환값을 unknown으로 개선함.
function parseYAMLFix(yaml: string): unknown {
    return parseYAML(yaml);
}

const book = parseYAMLFix(`
    name: asdfsafd
    author: abbbb
`);

alert(book.title); // 타입 채킹에서 오류 발생. Object is of type 'unknown'.
book(); // 타입 채킹에서 오류 발생. Object is of type 'unknown'.

우선 위 예제와 같이 unknown으로 함수의 반환 값을 변경했을 때, 타입 체킹 단계에서 좀 더 안전장치를 부여한 것처럼 보입니다.

 

unknown 타입을 이해하기 위해서 할당 가능성의 관점에서 any를 생각해볼 필요가 있습니다.

any가 강력하면서도 위험한 양날의 검과 같은 이유는 아래 두 가지 특성을 가지기 때문입니다.

  a. 어떠한 타입이든 any 타입에 할당 가능합니다.

  b. any 타입은 어떠한 타입으로도 할당 가능합니다.

 

unknown은 앞의 any의 특성 'a'를 가지지만, 두 번째 속성 'b'는 가지지 않습니다.

  a. 어떠한 타입이든 unknown에 할당 가능합니다.

  b. unknown 타입은 unknown과 any에만 할당 가능합니다.

 

한편 never 타입은 unknown과 정 반대로 동작합니다.

  a. 어떠한 타입도 never에 할당 불가능합니다.

  b. never 타입은 어떠한 타입으로도 할당 가능합니다.

 

다시 unknown에 대해서 얘기를 해봅시다.

앞선 예제에서 확인한 결과, unknown 타입 상태의 변수를 사용하려고 하면 오류가 발생한다는 것을 알 수 있습니다.

그리고 unknown 타입인 함수를 호출하거나, unknown 타입에 어떠한 연산을 하려고 하면 오류가 발생합니다.

따라서, unknown 타입을 사용하려면 적절한 타입으로 강제로 변환해야 합니다.

// 아이템 38에서 설명한 함수의 반환시 any 지양하기 철학에 위반.
function parseYAML(yaml: string): any {
    // ...
}

// 함수의 반환값을 unknown으로 개선함.
function parseYAMLFix(yaml: string): unknown {
    return parseYAML(yaml);
}

interface Book {
    name: string;
    author: string;
}

const book = parseYAMLFix(`
    name: asdfsafd
    author: abbbb
`) as Book;  // unknown 타입을 강제로 Book 타입으로 단언

alert(book.title); // 오류. Book 타입에는 title 속성은 없음.
book(); // 오류. Book 타입은 함수가 아님.

위와 같이 unknwon 타입을 강제로 변환해서 사용하면 타입 체커가 오류에 대한 보다 명확한 정보를 알려줍니다.

 

이어서, 변수 선언과 관련된 unknown을 알아보겠습니다.

function processValue(val: unknown) {
    if(val instanceof Date) {
        val.getDate(); // val이 Date 타입.
    }
}

function isBook(val: unknown): val is Book {
    return (
        typeof(val) === 'object' && val !== null &&
        'name' in val && 'author' in val
    );
}

function processValue2(val: unknown){
    if(isBook(val)){
        console.log(val.name); // 타입이 Book
    }
}

unknown 타입이 instancof 연산을 통해 타입 변환이 동작합니다. 또한 사용자 정의 타입 가드를 통해서도 타입의 변환이 일어나는 것을 확인할 수 있습니다.

 

마지막으로, 단언문과 관련된 unknown을 알아보겠습니다.

이중 단언문에서 any 대신 unknown을 사용할 수도 있습니다.

declare const foo: Foo;
let barAny = foo as any as Bar; // 불안전함.
let barUnk = foo as unknown as Bar; // 상대적으로 안전함.

barAny와 barUnk는 기능적으로 완전 동일하지만, 그 안정성 측면에서 차이를 보입니다. barAny는 any로 분리되는 순간 그 영향력이 예측 가능한 범위로 확산될 여지가 높아지지만, unknown으로 분리되면 any로 분리했을 때 발생할 예측 하기 어려운 부분을 타입 체커가 확인을 돕습니다.

 

번외로 unknown과 비슷하지만 object와 '{}'를 활용 사례도 알아보도록 하겠습니다.

object 또는 {}를 활용하는 방법 역시 unknown만큼 범위가 넓지만, 상대적으로 범위가 약간 좁습니다.

  a. {} 타입은 null과 undefined를 제외한 모든 값을 포함합니다.

  b. object 타입은 모든 비기본형(non-primitive) 타입으로 이루어집니다. true, 1, "test" 와같은 값들은 포함하지 않지만 객체와 배열은 포함이 됩니다.

 

[요약]

* unknwon은 any 대신 사용할 수 있는 안전한 타입입니다. 어떠한 값이 있지만 그 타입을 알지 못하는 상황이라면 unknown을 사용하는 것이 좋습니다.

* 사용자가 타입 단언문이나 타입 체크를 사용하도록 강제하려면 unknown을 사용하면 됩니다.

* {}, object, unknown의 차이점을 이해해야 합니다.