본문 바로가기
프로그래밍/타입스크립트

타입스크립트 이해

by Programmer.Junny 2025. 2. 13.

1. 기본 타입 호환

/**
 * unknown 타입
 */

function unknownExam() {
    let a: unknown = 1;
    let b: unknown = 'hello';
    let c: unknown = true;
    let d: unknown = null;
    let e: unknown = null;

    let unknownVar: unknown;

    // unknown 타입은 모든 타입들의 부모이므로 다운캐스팅 불가
    // let num: number = unknownVar;
    // let str: string = unknownVar;
    // let bool: boolean = unknownVar;
}

/**
 * never 타입
 */

function neverExam() {
    function neverFunc(): never {
        while(true) {}
    }

    // never 타입 모든 타입의 자식이므로 업캐스팅이 가능함
    let num: number = neverFunc();
    let str: string = neverFunc();
    let bool: boolean = neverFunc();
}

/**
 * void 타입
 */

function voidExam() {
    function voidFunc(): void {
        console.log('log');
    }

    let voidVar: void = undefined;
}

/**
 * any 타입 (계층도를 무시하는 치트키 타입)
 */

function anyExam() {
    let unknownVar: unknown;
    let anyVar: any;
    let undefinedVar: undefined;
    let neverVar: never;

    anyVar = unknownVar;

    undefinedVar = anyVar;

    //neverVar = anyVar; -> 에러 발생
}

객체 관계도를 살펴보면 이해하기가 쉽다.

주로 다운캐스팅이 불가능하다고 생각하면 되며, any타입만을 예외로 생각하면 된다.

2. 객체 타입의 호환성

type Animal = {
    name: string;
    color: string;
};
  
type Dog = {
    name: string;
    color: string;
    breed: string;
};

let animal: Animal = {
    name: "기린",
    color: "yellow",
};

let dog: Dog = {
    name: "돌돌이",
    color: "brown",
    breed: "진도",
};

animal = dog; // ✅ OK
//   dog = animal; // ❌ NO

let parentDog: Animal = dog;    // ✅ OK

위의 코드에서 Animal 타입과 Dog 타입에 같은 프로퍼티가 명시되었을 경우, 더 적은 공통 프로퍼티를 가진 타입이 부모타입이 된다.

위의 경우는 Animal이 Dog보다 프로퍼티가 적으므로 부모 프로퍼티가 되어 animal에 '업캐스팅' 할 수 있다.

3. 대수 타입

여러 개의 타입을 합성해서 새롭게 만들어낸 타입.

합집합 타입과 교집합 타입이 존재한다.

3.1. 합집합 타입 (Union 타입)

let a: string | number | boolean;
a = 1;
a = '문자열';
a = true;

기본 타입을 합집합 타입으로 만들 수 있다.

type Dog = {
    name: string,
    color: string,
};

type Person = {
    name: string,
    language: string,
}

type Union1 = Dog | Person;

또한 사용자가 새롭게 만든 타입들의 합집합 타입도 만들 수 있다.

let union1: Union1 = {
    name: '',
    color: '',
};

let union2: Union1 = {
    name: '',
    language: '',
};

let union3: Union1 = {
    name: '',
    color: '',
    language: '',
}

// 아래 타입은 Dog나 Person의 합집합이 아니므로 에러
// let union4: Union1 = {
//     name: '',
// }

Union1 은 Dog와 Person의 합집합 타입이므로 union4는 해당되지 않는다. (name만 존재하는 타입은 존재하지 않음)

3.2. 교집합 타입 (Intersection 타입)

let var1: number & string;  //교집합이 없으므로 never 타입

마찬가지로 기본 타입도 교집합 타입이 가능하나, 둘의 교집합을 만족하는 타입은 기본적으로 없으므로 never 타입이 된다.

//Dog 와 Person의 교집합 타입이므로 전부 포함해야 한다.
type Intersection = Dog & Person;

let intersection1: Intersection = {
    name: '',
    color: '',
    language: '',
}

4. 타입 추론

// number 타입으로 타입 추론
let a = 10;

// string 타입으로 추론
let b = "hello";

값을 대입한 경우 타입스크립트는 자동으로 해당 타입을 추론한다.

// id, name, profile, urls 프로퍼티가 있는 객체 타입으로 추론
let c = {
  id: 1,
  name: "junny",
  profile: {
    nickname: "nick",
  },
  urls: ["google.com"],
};

객체 또한 알아서 타입 추론 한다.

// 구조분해할당 타입 추론
let { id, name, profile } = c;

let [one, two, three] = [1, "hello", true];

구조 분해 할당도 타입 추론한다.

// 반환값이 string 타입으로 추론된다
function func() {
  return "hello";
}

함수는 반환값 기준으로 타입을 추론하므로, func는 string 타입이다.

// 10 Number Literal 타입으로 추론
const num = 10;

//  num = 20;   //타입스크립트에서의 const는 리터럴 상수 타입이기 때문에 에러 발생

const 의 경우 자바스크립트와는 다르게 추가 대입이 불가능하다. (진정한 상수가 된다.)

5. 타입 단언

let num1 = 10 as never;
let num2 = 10 as unknown;

슈퍼타입이거나 서브타입인 경우에 타입 단언을 실행할 수 있다.

/**
 * const 단언
 */

let num4 = 10 as const; //let을 const로 만드는 것과 같음

//각 프로퍼티에 readonly를 사용한 것과 같음
let cat = {
    name: '야옹이',
    color: 'yellow',
} as const;

const 단언에서 객체를 as const 하면 모든 프로퍼티에 readonly 키워드가 붙는다.

/**
 * Non Null 단언
 */

type Post = {
    title: string,
    author?: string,
}

let post: Post = {
    title: '게시글1',
    author: '사용자1',
}

const len: number = post.author!.length;

Non Null 단언은 프로그래머가 임의로 해당 속성이 있다고 단언하는 것이다.

6. 타입 좁히기

/**
 * 타입 좁히기
 * 조건문 등을 이용해 넓은 타입에서 좁은 타입으로
 * 타입을 상황에 따라 좁히거나 넓히는 것
 */

type Person = {
    name: string,
    age: number,
}

function func(value: number | string | Date | null | Person) {
    if (typeof value === 'number') {
        console.log(value.toFixed());
    } else if (typeof value === 'string') {
        console.log(value.toUpperCase());
    } else if (value instanceof Date) {
        console.log(value.getDate());
    } else if (value && 'age' in value) {
        console.log(`${value.name} 은 ${value.age}살 입니다.`);
    }
}

기본적인 타입은 typeof 를 사용하지만, Date와 같은 객체나 null의 구분을 위해선 instanceof 를 사용해야 한다.

그러나 사용자가 생성한 타입은 객체가 아니므로 instanceof를 사용할 수 없으므로 in 키워드를 사용한다.

value && 'age' 인 이유는 value가 있음을 보장하는 것이다. 

value !== null && 'age' 와 같다고 보면 된다.

7. 서로소 유니온 타입 (Tagged 유니온 타입)

enum Tag {
    ADMIN,
    MEMBER,
    GUEST,
}

type Admin = {
    tag: Tag.ADMIN,
    name: string,
    kickCount: number,
};

type Member = {
    tag: Tag.MEMBER,
    name: string,
    point: number,
};

type Guest = {
    tag: Tag.GUEST,
    name: string,
    visitCount: number,
};

type User = Admin | Member | Guest;

Admin, Member, Guest 는 tag 를 기준으로 연결되어 있다.

// Admin -> {name}님 현재까지 {kickCount}명 강퇴했습니다.
// Member -> {name}님 현재까지 {point} 모았습니다.
// Guest -> {name}님 현재까지 {visitCount}번 오셨습니다.
function login(user: User) {
    switch (user.tag) {
        case Tag.ADMIN:
            console.log(`${user.name}님 현재까지 ${user.kickCount}명 강퇴했습니다.`);
            break;
        case Tag.MEMBER:
            console.log(`${user.name}님 현재까지 ${user.point} 모았습니다.`);
        break;
        case Tag.GUEST:
            console.log(`${user.name}님 현재까지 ${user.visitCount}번 오셨습니다.`);
        break;
    }
}

User는 Admin, Memeber, Guest 중 하나이면서 tag를 포함하고 있으므로, tag를 기준으로 나눌 수 있다.

type LoadingTask = {
    state: 'LOADING',
}

type FailedTask = {
    state: 'FAILED',
    error: {
        message: string,
    },
}

type SuccessTask = {
    state: 'SUCCESS',
    response: {
        data: string,
    },
}

type AsyncTask = LoadingTask | FailedTask | SuccessTask;

만약 위와 같은 서로소 유니온 타입을 만들었다면

function processResult(task: AsyncTask) {
    switch (task.state) {
        case 'LOADING':
            console.log('로딩 중');
            break;
        case 'FAILED':
            console.log(`에러 발생 : ${task.error.message}`);
            break;
        case 'SUCCESS':
            console.log(`성공 : ${task.response.data}`);
            break;
    }
}

각 케이스에서 이미 해당 타입(LoadingTask 또는 FailedTask 또는 SuccessTask)임을 나타내므로 error 나 response를 쓸 수 있게 된다. (옵셔널 체이닝이나 Non null을 하지 않아도 된다.)

최근댓글

최근글

skin by © 2024 ttuttak