[TypeScript] TS의 type

Hoplin·2023년 2월 26일
0

TypeScript의 Type Annotation

Annotation을 사용하여 명시적으로 타입을 지정할 수 있다. 아래와 같은 형태로 작성을 한다

value:type

타입스크립트 코드로 예시를 들면

let a:number = 1
let b:string = 'string'
let c:boolean[] = [true,false,true]

일반적으로 타입스크립트가 타입을 추론하도록 하여 어노테이션을 생략한다.

TypeScript Type Hirarchy

any

모든 타입을 가리키는 타입이다. 기본적으로 타입 스크립트가 타입을 추론하지 못하는 경우 any로 가정을 한다. 단, 가급적 사용하지 않는것이 좋다. 상황에 따라 어떤 동작을 할지 보장하지 못할 수 도 있기 때문이다.

function anyExample(arg: any[]): void {
    arg.map(x => {
        console.log(x.charAt(0))
    })
}

let stringArr: string[] = ['abc', 'def', 'ghi']
anyExample(stringArr)
let numberArr: number[] = [1, 2, 3, 4, 5];
anyExample(numberArr) //TypeError: x.charAt is not a function

위 예시에서 anyExample함수는 매개변수로 any타입 배열을 받는다. 배열의 요소를 순회하며, 각 요소에 대해 charAt() 메소드를 실행하는 기능을 가지고 있다. stringArr처럼 요소가 string타입인 배열이면 괜찮겠지만, numberArr같은 경우, number타입은 charAt() 메소드가 없기때문에 오류가 난다.

하지만 문제점은 컴파일은 정상적으로 된다는 것이다. 컴파일 과정에서 타입 오류를 줄이고자 하는 타입 스크립트 의도에서 벗어나게 된다.

tsconfig.json에서 noImplicitlyAny 플래그를 설정해 any가 나타난 경우 예외가 나게 할 수 있다.

unknown

any와 동일하게 타입을 미리 알 수 없는 경우 사용한다. 하지만 타입을 알 수 없는 경우, unknown을 사용하는것이 any를 사용하는거보다 안전하다. 그 이유는 unknown 타입은 아래 예시와 같이 타입을 정제하기 전까지는 unknown 타입 값을 사용하지 못하도록 강제한다.

function unknownExample(arg: unknown[]): void {
    arg.map(x => {
        //'x'은(는) 'unknown' 형식입니다.ts(18046)
        x.charAt(0);
    })
}

위 코드를 사용하기 위해서는 아래와 같이 정제 과정이 필요하다

function unknownExample(arg: unknown[]): void {
    arg.map(x => {
        if (typeof x === 'string') {
            console.log(`String : ${x.charAt(0)}`)
        }
        else if (typeof x === 'number') {
            console.log(`Number : ${x}`)
        } else {
            console.log(`Unknown : ${x}`)
        }
    })
}
unknownExample(stringArr)
unknownExample(numberArr)

boolean, bigint, number, string

boolean,number,string,bigint 타입을명시하는 방식들은 아래와 같은 방식들이있다.

  1. 추론하는 방식
let a = true // TypeScript가 boolean으로 추론
  1. 리터럴 방식
let a:true = true // 무조건 true값만 올 수 있음
  1. 타입을 명시
let a:boolean = true // true, false 모두 가능

boolean

true 혹은 false의 값을 가지며, 비교연산, 반전연산을 할 수 있다.

let b1: boolean = true
let b2: true = true
let b3 = true

number

정수, 소수, 양수 음수, Infinity, NaN을 모두 포함하는 집합이며, 사칙연산, 모듈로, 비교 연산이 가능한 값들이다.

let n1: number = 10
let n2: 26.15 = 26.15
let n3 = 25

bigint

큰 정수를 처리할때 사용하며, 2^53보다 큰 값들을 처리하며 사칙연산, 비교연산등을 지원한다. 정수 리터럴 뒤에 n을 붙이는 특징이 있으며 주의할 점은 bigint 리터럴은 무조건 정수여야한다. bigint는 플랫폼별로 지원 여부가 다르고 ES2020 이상에서 지원한다

let bi1 = 10n;
let bi2: bigint = 100n;
let bi3: 20n = 20n;
// let bi4: bigint = 100.41n; // 리터럴은 정수여야함

string

모든 문자열의 집합이며, 일반적인 모든 string타입 연산이 가능하다.

let s1 = 'string';
let s2: 'string2' = 'string2';
let s3: string = 'string3';

객체

자바스크립트는 구조기반 타입화라는것을 짚고가자. 이는 객체가 어떤 프로퍼티를 갖고 있는지에 따라 타입을 판별한다는 이야기이다. 타입 스크립트에서 객체 타입 명시를 위해서는 어떻게 해야할까?

object로 명시하기

object는 말그대로 객체임을 의미한다. 예를 들어 아래와 같이 작성했다 가정한다.

let obj1: object = {
    a: 'x'
}
obj1.a;//error TS2339: Property 'a' does not exist on type 'object'

컴파일 과정에서 오류가생긴다. 이유는 object는 단지 값 자체가 객체라는것을 알려줄 뿐 그 안에 어떤 정보가 있는지 알려주지 않기 때문이다. 당연히 타입을 주지않으면, 타입스크립트가 추론한다. 반대로 객체의 타입을 명시해주는 방식도 있다. 명시적으로 객체의 타입을 주기 위해서는 중괄호를 이용하여 객체의 각 프로퍼티별 타입을 명시한다. 위 예시의 컴파일 오류를 없애보자

let obj2: {
    a: string
} = {
    a: 'x'
}

console.log(obj2.a)

// 다른 예시
let c: {
    firstname: string,
    secondname: string,
    height: number
} = {
    firstname: 'yoon',
    secondname: 'hoplin',
    height: 172
}

위처럼 객체 프로퍼티를 명시한 경우에는 무조건 프로퍼티의 값을 주어야 한다.

선택적 프로퍼티

경우에 따라 프로퍼티가 선택적으로 존재할 수 도 있다. 그런경우에는 ?를 붙여 선택적 프로퍼티임을 나타낼 수 있다.

let sp: {
    b: number,
    c?: string
};
sp = {
    b: 10
}

sp = {
    b: 20,
    c: 'selective string'
}

위 예시의 경우, sp타입에 들어올 객체는 b라는 프로퍼티는 필수로, c라는 프로퍼티는 선택적으로 가질 수 있게된다.

index signature

[key: T]:U 형태로 프로퍼티 타입을 지정해 줄 수 있다. 이를 해석하면, 이 객체에서 T타입 키는 U 타입의 값을 갖는다는 의미이다. 이를 사용하면 명시적으로 정의한 필드 이외의 필드를 추가하기 자유롭다.

단 인덱스 시그니처에는 규칙이 하나 있다. 키에 해당하는 값으리 자료형은 number혹은 string, Symbol타입에 할당할 수 있어야한다.

인덱스 시그니처 사용시 주의할 점이있다. 아래와 같이 객체 타입을 선언했다 가정하면, 컴파일 오류가 난다.

//error TS2411: Property 'b' of type 'number' is not assignable to 'string' index type 'boolean'.
//error TS2411: Property 'c' of type 'string | undefined' is not assignable to 'string' index type 'boolean'.
let sp2: {
    b: number,
    c?: string,
    [key: string]: boolean
}

오류가 나는 이유는, 명시적 프로퍼티 키(key)인 b,c는 문자열타입으로 간주된다.(이 키들은 각각 "b", "c"와 같이 리터럴 형태로 명시한것과 동일한 효과이다.). 하지만 인덱스 시그니처에서, string 타입은 boolean을 가진다고 명시를 하였다.

string은, "b","c"보다 더 상위 스코프 타입이다.그렇기에 위 코드에서 컴파일 오류가 나지 않도록 하기 위해서는 뒤에서 볼 유니온 연산자( | ) 를 통해 인덱스 시그니처의 value의 자료형들을 추가해 주어야한다.

let sp2: {
    b: number,
    c?: string,
    [key: string]: boolean | number | string | undefined // undefined는 c가 선택적 프로퍼티이므로
}

sp2 = {
    b: 10,
    c: 'hello',
    d: 10,
    e: true,
    f: false,
    g: 'string'
}

물론 위 코드에서는 명시적인 number타입의 키가 없으므로, 인덱스 시그니처의 키 타입을 number로 해도 문제되지 않는다.

let sp2: {
    b: number,
    c?: string,
    [key: number]: boolean
}

인덱스 시그니처 활용 예시를 하나 더 들어본다. 아래와 같이 타입별로 분류를 할 수 도 있다.

let sp3: {
    b: number,
    c?: string,
    [key: string | symbol]: boolean | number | string | undefined,
    [numType: number]: boolean
}

sp3 = {
    b: 100,
    10: true,
    e: 'hello'
}

타입 별칭

타입 별칭을 통해 타입을 가리킬 수 도 있다. 쉽게 말해 사용자 정의 타입을 만들 수 있는것이다.type키워드를 이용하며, 앞에서 봤던것과 같이 코드를 작성하면, 가독성 면에서 매우 떨어지고 지저분해 진다. 이러한 문제를 해결할 수 도 있다. 형태는 아래와 같다

type (type name) = (type value)
type Age = number;
type Person = {
    name: string,
    age: Age
}


type sptype = {
    b: number,
    c?: string,
    [key: string | symbol]: boolean | number | string | undefined,
    [numType: number]: boolean
}

let sp4: sptype;
sp4 = {
    b: 100,
    10: true,
    e: 'hello'
}

유니온과 인터섹션

유니온과 인터섹션 문법은, 타입스크립트 문법에 적용하는 연산자들이다.

유니온 연산은 | 기호를 사용하며, 유니온을 구성하는 타입 중 하나이거나 모두일 수 있다.

인터섹션은 &기호를 사용하며, 인터섹션을 구성하는 모든 타입을 만족해야한다.

type Cat = {
    name: string,
    purrs: boolean
}

type Dog = {
    name: string,
    barks: boolean,
    wags: boolean
}

type CatOrDogOrBoth = Cat | Dog
type CatAndDog = Cat & Dog

let a: CatOrDogOrBoth = {
    name: 'meow',
    purrs: true
}

let b: CatOrDogOrBoth = {
    name: 'bark',
    barks: true,
    wags: true
}

let ab: CatAndDog = {
    name: 'CatDog',
    purrs: true,
    barks: true,
    wags: true
}

배열

일반적인 배열이다. concat,splice,push등 배열 연산이 모두 가능한 객체이다. 배열 객체 타입지정은 아래와 같이할 수 있다. 여기서 타입은 배열의 요소들의 타입을 의미한다. 물론, 타입을 지정하지 않으면 초기화를 하면서 타입을 자동으로 추론한다.

// 1차원배열
타입[]

//2차원배열
타입[][]

코드로 예시를 들어보자

let arr1: string[] = ['a', 'b']
let arr2 = [1, 2, 3, 4, 5] // number[] 로 추론

위에서 본 유니온 연산자를 이용해서 배열을 타입을 하나가 아닌 여러개로 지정할 수 도 있다(동형배열).

let arr4: (string | boolean)[] = ['arr1', true, false, 'arr2']

만약 초기화를 빈배열로 한다면 any[]로 추론한다. 빈배열로 초기화해 any[]로 추론된 배열은 배열 조작에 따라 타입이 추정되며, 배열이 정의된 블럭에서 벗어나면, 배열 타입을 더이상 확장하지 못한다. 아래 예시에서 arrTest()의 배열 a는 any[]로 초기화 된 후 조작에 의해(number | boolean)[]으로 추론이 되다 반환되는 순간, 고정이 된다.

let arr5 = [] // any[]
arr5.push(true); // boolean[]
arr5.push('red') // (string | boolean)[]

function arrTest(): (number | boolean)[] {
    let a = []; 
    a.push(1);
    a.push(true)
    return a;
}

튜플

배열의 서브 타입인 튜플은 길이가 고정되어있으며 각 인덱스의 타입이알려진 배열의 일종이다. 자바스크립트에서는 기본적으로 []를 배열로 인식하기 때문에 튜플은 선언시 타입을 명시해야 줘야한다. 타입 명시하는 방법은 배열과 조금 다르다

[타입, 타입,...etc]

코드로 예시를 들어보자

let tple: [number] = [1] // 하나의 인자만 올 수 있다.
let tple2: [string, string, number] = ['arr', 'tple', 10]
tple2 = [10, 20, 30] // 'number' 형식은 'string' 형식에 할당할 수 없습니다.ts(2322)

객체 타입처럼 선택형 요소를 지정할 수 도 있다.

// tuple 타입 배열
let tple3: [number, number?][] = [[10], [10, 20], [30]]

// 이는 위와 같은 의미이다
let tple3: ([number] | [number,number])[] = [[10], [10, 20], [30]]

튜플은 이형 배열(서로 형태가 다른 배열)을 안전하게 관리할 뿐 아니라 배열 타입의 길이도 조절할 수 있다. 예를 들어 최소 3개의 number 요소가 들어있음이 보장되어야 하고 그 이상 개수의 number 요소는 유동적으로 받는 튜플이라고 가정하면 Rest Parameter를 이용해서 나타낼 수 있다.

let tple4: [number, number, number, ...number[]] = [1, 2, 3, 4, 5, 6, 7];

Rest Parameter를 이용해서 배열을 한번에 나열할 수 있는 점을 이용한것이다. 배열에서 4,5,6,7은 [4,5,6,7]을 나열한것과 동일하다.

> const a = [1,2,3,4]
undefined
> console.log(...a)
1 2 3 4
undefined

이 기능을 잘 활용하면 순수 배열에 비해 안정성을 높일 수 있다.

null, undefined, void, never

타입의미
null값이 없음
undefined아직 값을 변수에 할당하지 않음
voidreturn문을 포함하지 않는 함수
never절대 반환하지 않는 함수(예외, 무한루프 등)

enum

enum은 열거형으로, 해당 타입으로 사용할 수 있는 값을 열거하는 타입이다. 주로 값들을 미리 정하고 사용할때 쓴다. enum은 안전하게 사용하는 방식만 소개한다. 기본적인 열거형 타입은 아래와 같다. 기본적으로 키만 지정하게 되면, 0부터 차례대로 숫자가 대입된다

enum Language {
    English,
    Korean,
    Spanish,
    Russian
}

console.log(Language.English); //0
console.log(Language.Korean)// 1
console.log(Language.Spanish)// 2
console.log(Language.Russian)// 3

될 수 있으면, 열거형을 사용할때는 값을 명시적으로 대입하는것이 좋다. 값에는 문자열과 숫자 두가지를 사용할 수 있다.

enum Language2 {
    English = 0,
    Korean = 'korean'
}

열거형은 키뿐만 아니라 값으로도 접근할 수 있다. 다만, 일반적인 열거형은 접근이 불가능한것에 대해서도 접근이 되기 때문에, 안전한 열거형 타입을 사용하기 위해서는 const enum을 사용해야한다. const enum은 값을 통한 찾기를 지원하지 않는다.

enum Language2 {
    English = 0,
    Korean = 'korean'
}

console.log(typeof Language2[10]) // undefined

const enum Language3 {
    Spanish = 10,
    Japanese = 20
}
console.log(typeof Language3[10]); // const 열거형 멤버는 문자열 리터럴을 통해서만 액세스할 수 있습니다.ts(2476)

typescript 열거형은 안전하게 사용하는 방법이 까다롭기때문에, 사용이 비권장된다. 대신 다른 방식으로 대체할 수 있다.

/**
 * as const는 객체나 배열도 const로 선언한 원시 값의 타입처럼, 리터럴 타입의 추론 범위를 줄이고 값의 재할당을 막아준다(readonly)
 */
const status = {
    todo: 'todo',
    inProgress: 'Inprogress'
} as const

//typeof : 객체 데이터를 객체 타입으로 변환해주는 연산자
//keyof : 객체 형태의 타입을, 따로 키값들만 뽑아 모아 유니온 타입으로 만들어주는 연산자
type Status = typeof status[keyof typeof status]
profile
더 나은 내일을 위해 오늘의 중괄호를 엽니다

0개의 댓글