[하루 30분 러닝 타입스크립트] 타입 제한자

✨ top 타입

top 타입은 시스템에서 가능한 모든 값을 나타내는 타입입니다.

any

any 타입은 top 타입처럼 작동할 수 있지만, 타입 검사를 수행하지 않도록 명시적으로 지시합니다. 즉 타입 오류를 보고하지 않습니다.

let anyValue: any;
anyValue = "ABC" // OK
anyValue = 123 // OK

function greet(name: any){
    console.log(`hi ${name.toUpperCase()}!`)
}

greet({name: "ABC"}) // OK

unknown

unknown 타입은 진정한 top타입 입니다. 타입의 값을 제한적으로 취급합니다.

function greet(name: unknown){
    console.log(`hi ${name.toUpperCase()}!`)
    // ERROR 'name' is of type 'unknown'.
}

위 코드에서 unknown 타입에 접근하기 위해서는 instanceof 혹은 typeof 키워드를 통해 타입 어서션을 사용하여 타입을 제한하여야 합니다.

function greet(name: unknown){
    if(typeof name === "string"){
        console.log(`hi ${name.toUpperCase()}!`)
    } // OK
}

✨ 타입 서술어

instanceof 혹은 typeof 구문을 사용해 타입을 좁힐 수 있지만, 로직을 함수로 감싸면 타입을 알 수 없게 됩니다.

function isString(value: unknown) {
    return ['string'].includes(typeof value)
}

function greet(name: unknown){
    if(isString(name)){
        console.log(`hi ${name.toUpperCase()}!`)
    } // ERROR 'name' is of type 'unknown'.
}

타입 스크립트에는 인수가 특정 타입인지 여부를 나타내기 위해 **boolean 값을 반환하는 함수를 위한 특별한 구문**이 있습니다. 이를 타입 서술어 라고 합니다.

이는 매개면수의 이름, is 키워드, 특정 타입으로 선언할 수 있습니다.

function isString(value: unknown): value is string {
    return ['string'].includes(typeof value)
}

function greet(name: unknown){
    if(isString(name)){
        console.log(`hi ${name.toUpperCase()}!`)
    } // OK
}

타입 서술어는 한 인터페이스의 인스턴스로 알려진 객체가 더 구체적인 인터페이스의 인스턴스인지 여부를 검사하는 데 자주 사용됩니다.

class Man {
    strength: number

    constructor(value: number) {
        this.strength = value
    }
}

class SuperMan extends Man {
    isFly: boolean

    constructor(value: number){
        super(value)
        this.strength *= 1000
        this.isFly = true
    }
}

function isSuperman(value: Man): value is SuperMan {
    return 'isFly' in value
}

function isFlyPeople(value: Man) {
    if(isSuperman(value)){
        console.log(value.isFly) // OK
    }
    console.log(value.isFly) // ERROR Property 'isFly' does not exist on type 'Man'.
}

타입 서술어는 타입을 확인하는 것 이상으로 수행하는 것은 지양해야 합니다. 간단한 타입 서술어만으로도 충분합니다.

✨ 타입 연산자

keyof

자바스크립트는 string타입의 동적 값을 통해 멤버를 찾는 경우가 있습니다. 하지만 다음과 같은 경우는 에러가 발생합니다.

interface Ratings {
    audience: number;
    critics: number;
}

function getRating(rating: Ratings, key: string){
    return rating[key] 
    /* ERROR
    Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Ratings'.
    No index signature with a parameter of type 'string' was found on type 'Ratings'.
    */
}

이러한 경우 keyof 키워드를 사용할 수 있습니다.

interface Ratings {
    audience: number;
    critics: number;
}

function getRating(rating: Ratings, key: keyof Ratings){
    return rating[key] // OK
}

keyof Ratings 는 audience | critics 타입과 동일하지만, 작성하기 빠르고 수동으로 업데이트 할 필요가 없습니다.

typeof

아래의 copy 변수는 original 과 동일한 타입 값을 가집니다.

const original = {
    title : "ABC",
    description : "ABCDE"
}

let copy: typeof original

keyof typeof

keyof는 타입에 허용된 키를 검색하고, typeof 는 값의 타입을 검색합니다.

두 키워드를 함께 사용하면, 작업할 때 매우 유용합니다.

const original = {
    title : "ABC",
    description : "ABCDE"
}

function logValues(key: keyof typeof original) {
    console.log(original[key])
}

✨ 타입 어서션

타입 시스템을 100% 정확하게 알리는 것이 불가능한 상황이 있습니다.

JSON.parse 는 의도적으로 top타입인 any를 반환합니다.

타입 어서션을 이용하여 타입을 설정할 수 있습니다. as 키워드를 사용합니다.

const rawData = `["abc", "ABC"]`;

JSON.parse(rawData) as string[];

하지만 타입 어서션을 되도록 지양하세요.

포착된 오류 타입 어서션

오류를 처리할 때 타입 어서션이 매우 유용합니다.

try{
    //...
} catch(error) {
    console.log((error as Error).message)
}

일부 프로젝트에서는 문자열 리터럴 또는 다른 의외의 값을 발생시키기도 합니다. 따라서 내로잉 하는 것이 더 안전합니다.

try{
    //...
} catch(error) {
    console.log(error instanceof Error ? error.message : error)
}

non-null 어서션

이론적으로 null 또는 undefined를 포함할 수 있는 변수에서 null과 undefined를 제거할 때 타입 어서션을 주로 사용합니다.

let maybeDate = Math.random() > 0.5 ? undefined : new Date();

const thisMustDate = maybeDate as Date
// const thisMustDate: Date

!를 사용하여 전체타입 대신 작성할 수 있습니다.

const hashMap = new Map([
    ["ABC", "123"],
    ["DEF", "456"]
])

const valueOfABC = hashMap.get("ABC")
console.log(valueOfABC.toUpperCase()) // ERROR 'valueOfABC' is possibly 'undefined'.

const valueOfDEF = hashMap.get("DEF")!
console.log(valueOfDEF.toUpperCase()) // OK

타입 어서션 주의 사항

  1. 타입 어서션은 도피 수단의 하나입니다. 따라서 자주 사용해선 안됩니다. 확실히 안전한 상황에서만 사용하세요.
  2. 어서션과 선언은 다릅니다. 어서션은 타입 검사를 건너뛰도록 명시적으로 지시하는 것이고, 선언은 변수의 초기값에 대해 할당 가능성 검사를 수행합니다.
  3. 이중 타입 어서션을 사용하여 전혀 관련 없는 타입을 허용할 수 있습니다.하지만 이는 잘못된 코드의 징후입니다. 수정하세요.
  4. const myName = "lurgi" as unknown as number; // OK // const myName: number

✨ const 어서션

const 어서션은 배열, 원시 타입 값, 별칭 등 모든 값을 상수로 취급해야 함을 나타내는데 사용됩니다. 다음 규칙을 적용합니다.

  1. 배열은 가변 배열이 아닌 읽기 전용 튜플로 취급
  2. 리터럴은 일반적인 원시 타입과 동등하지 않고 리터럴로 취급
  3. 객체의 속성은 읽기 전용으로 간주

읽기 전용 튜플로 취급

const arr = [0, ""] // const arr: (string | number)[]
const arr2 = [0, ""] as const // const arr2: readonly [0, ""]

읽기 전용 객체

const obj = {
    name: "lurgi"
} as const

obj.name = "ABC"
// ERROR Cannot assign to 'name' because it is a read-only property.

리터럴에서 원시 타입으로

const getName = () => "lurgi" // const getName: () => string
const getName2 = () => "lurgi" as const // const getName2: () => "lurgi"

다음과 같은 상황이 발생할 수 있습니다.

interface Style {
    style : "style1" | "style2"
}

function printStyle(obj : Style){
    if(obj.style === "style1"){
        console.log("style1")
    }
    if(obj.style === "style2"){
        console.log("style2")
    }
}

const obj = {
    style : "style1"
}
printStyle(obj)
/* ERROR 
    Argument of type '{ style: string; }' is not assignable to parameter of type 'Style'.
    Types of property 'style' are incompatible.
    Type 'string' is not assignable to type '"style1" | "style2"'.
*/

const obj2 = {
    style : "style1" as const
}
printStyle(obj2) // OK