러닝타입스크립트 4장 - 객체
포스트
취소

러닝타입스크립트 4장 - 객체

기존 블로그에 작성했던 스터디 포스트를 이전한 글입니다.

해당 포스트는 TypeScript를 학습하며 정리한 내용에 대한 포스트입니다.




🌈 객체

📖 4.1 객체 타입

  • {...} 구문을 사용해서 객체 리터럴을 생성하면, TS는 해당 속성을 기반으로 새로운 객체 타입 또는 타입 형태를 고려한다.

  • 해당 객체 타입은 객체의 값과 동일한 속성명과 원시 타입을 갖는다.

  • 값의 속성에 접근하려면 value.멤버또는value[“멤버”]를 구문을 사용한다.

1
2
3
4
5
6
7
8
9
const someone = {
	born : 1990,
  	name : "leekoby" 
};
someone["born"] // 타입 : number
someone.name // 타입 : string

someone.hello; 
// ❌ Error : Property 'hello' does not exist on type '{ born: number; name: string; }'.
  • 객체 타입은 TypescriptJavascript를 이해하는 방법에 대한 핵심 개념

  • nullundefined를 제외한 모든 값은 그 값에 대한 실제 타입의 멤버 집합을 가짐.

  • 그렇기 때문에 Typescript는 모든 값의 타입을 확인하기 위해서 객체 타입을 이해해야한다.


4.1.1 객체 타입 선언

  • 기존 객체에서 직접 타입을 유추하는 방법도 굉장히 좋지만, 객체의 타입을 명시적으로 선언하고 싶음

  • 명시적으로 타입이 선언된 객체와는 별도로 객체의 형태를 설명하는 방법 필요

  • 객체 타입은 객체 리터럴과 유사하게 보이지만 필드 값 대신 타입을 사용해 설명

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let someone = {
    born: 1990,
    name: "leekoby"
};

someone = {
    born: 1333,
    name: "kobykoby"
};
someone["born"] // 타입 : number
someone.name // 타입 : string

someone = "cube"
// ❌ Error : Type 'string' is not assignable to type '{ born: number; name: string; }'.


4.1.2 별칭 객체 타입

  • 객체 타입을 계속 장성하는 일은 효율성이 떨어진다.

  • 객체의 타입에 타입 별칭을 할당해 사용하는 방법이 더 일반적이다.

  • ✅ 대부분의 타입스크립트 프로젝트는 객체 타입을 설명할 때 interface 키워드 사용을 선언한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type koby = {
    born: number;
    name: string;
}

let someone: koby
someone = {
    born: 1990,
    name: "leekoby"
};

someone = {
    born: 1333,
    name: "kobykoby"
};
someone["born"] // 타입 : number
someone.name // 타입 : string

someone = "cube"
// ❌ Error : Type 'string' is not assignable to type 'koby'.


대부분의 Typescript 프로젝트는 객체 타입을 설명할 때 interface

🦆 4.2 구조적 타이핑

  • 타입스크립트의 타입 시스템은 구조적으로 타입화 되어 있다.

    • 즉, 타입을 충족하는 모든 값을 해당 타입의 값으로 사용할 수 있다.

    • 매개변수나 변수가 특정 객체 타입으로 선언되면 타입스크립트에 어떤 객체를 사용하든 해당 속성이 있어야 한다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type WithFirstName = {
 firstName: string; 
};

type WithLastName = {
  lastName: string;
}

const hasBoth = {
  firstName: "Koby",
  lastName: "Lee"
};

// ✅ hasBoth는 string 타입의 "firstName"을 포함함
let WithFirstName: WithFirstName = hasBoth;

// ✅ hasBoth는 string 타입의 "lastName"을 포함함
let WithLastName: WithLastName = hasBoth;
  • 덕 타이핑 ( JavaScript ):
    • 런타임에서 사용될 때까지 객체 타입을 검사하지 않는 것
  • 구조적 타이핑 ( TypeScript ):
    • 정적 시스템이 타입을 검사하는 것


4.2.1 사용검사

객체 타입에 정의한 값을 필수적으로 선언해야 하고, 타입의 정보와도 일치해야한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Sth = {
  name: string;
  age: number;
};

//  Error: Property 'age' is missing in type '{ name: string; }' but required in type 'Sth'
const sth : Sth = {
  name: "Aatrox",
};

// Error: Type 'string' is not assignable to type 'number'
const sthBoth: Sth = {
  name: "Aatrox",
  age: "10",
};


4.2.2 초과 속성 검사

초기에 정한 타입보다 더 많은 속성을 사용하려하면 타입 오류가 발생

객체 리터럴이 아닌 경우에는 구조적 타이핑에 의해서 오류가 발생하지 않음

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
type Sth = {
    name: string;
    age: number;
};

// (3)
const Sth1: Sth = {
    name: "leekoby",
    age: 33,
    activity: 'working',
    // ❌ Error : Type '{ name: string; age: number; activity: string; }' is not assignable to type 'Sth'.
    // Object literal may only specify known properties, and 'activity' does not exist in type 'Sth'.

};

// (4)
const Sth = {
    name: "kobykoby",
    age: 22,
    activity: 'working',
};
// (4)
const Sth2: Sth = Sth;

// ❌ Error : Property 'activity' does not exist on type 'Sth'.
Sth2.activity;


4.2.3 중첩된 객체 타입

Javascript 객체는 다른 객체의 멤버로 중첩될 수 있으므로 타입스크립트의 객체 타입도 타입 시스템에서 중첩된 객체 타입을 나타낼 수 있어야한다

  • 중첩 객체의 속성의 형태를 자체 별칭 객체 타입으로 추출하면 오류 메시지에 더 많은 정보를 담을 수 있다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
type Someone = {
    author: {
        firstName: string;
        lastName: string;
    };
    name: string;
};

const someoneMatch: Someone = {
    author: {
        firstName: 'koby',
        lastName: 'lee'
    },
    name: 'Typescript',
};

const someoneMismatch : Someone = {
    author: {
        name: "React"  
/**
  ❌ Error: Type '{ name: string; }' is not assignable 
  to type '{ firstName: string; lastName: string; }'.
  Object literal may only specify known properties, and 'name' 
  does not exist in type '{ firstName: string; lastName: string; }'.
 */
    },  
    name: 'JavaScript',
};

객체의 속성으로 타입을 사용할 수 있기 때문에 위처럼 사용하는 것보다는 아래와 같이 사용하는 것이 가독성이 더 좋고, 오류 메세지도 보다 명확해진다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Someone = {
    firstName: string;
    lastName: string;
}
type doSomethong = {
    who: Someone;
    activity: string;
}
const someoneMismatch: doSomethong = {
    who: {
        name: "leekoby"
        /**
          ❌ Error : Type '{ name: string; }' is not assignable to type 'Someone'.
          Object literal may only specify known properties, 
          and 'name' does not exist in type 'Someone'.
          */
    },
    activity: 'run',
};


4.2.4 선택적 속성

?:을 이용해서 선택적 속성을 부여할 수 있다.

선택적 속성 ?: 대신 undefined를 사용할 수 있음

그러나 undefined를 유니언으로 선언했다면 속성 값을 반드시 선언한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
type Someone = {
  name: string;
  age?: number;
  work: boolean | undefined;
};
const person1: Someone = {
  name: "Aatrox",
  age: 26,
  work: false,
};
// ❌ Property 'work' is missing in type '{ name: string; age: number; }' but required in type 'Someone'.
const person2: Someone = {
  name: "Aatrox",
  age: 26,
};
const person3: Someone = {
  name: "Aatrox",
  work: false,
};
// `?:` 대신 undefined 
const person4: Someone = {
  name: "Aatrox",
  age: 26,
  work: undefined,
};


📖 4.3 객체 타입 유니언

  • TS 코드에서는 속성이 조금 다른, 하나 이상의 서로 다른 객체 타입이 될 수 있는 타입을 설명할 수 있어야 한다.

  • 속성값을 기반으로 해당 객체 타입 간에 타입을 좁혀야 할 수 도 있다.


4.3.1 유추된 객체 타입 유니언

  • 변수에 여러 객체 타입 중 하나가 될 수 있는 초깃값이 주어지면 TS는 해당 타입을 객체 타입 유니언으로 유추한다.

  • 유니언 타입은 가능한 각 객체 타입을 구성하고 있는 요소를 모두 가질 수 있다.

  • 객체 타입에 정의된 각각의 가능한 속성은 비록 초깃값이 없는 선택적 타입이지만 각 객체 타입의 구성 요소로 주어진다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const poem = Math.random() > 0.5
    ? { name: "leekoby", pages: 9 }
    : { name: "kimcube", rhymes: true }
/*
type:
{
    name : string;
    pages : number;
    rhymes?: undefined;  
}
|
{
    name : string;
    pages ?: undefined;
    rhymes : boolean;  
}
 */
poem.name //string
poem.pages; // number | undefined
poem.rhymes; // booleans | undefindes

다음 poem 값은 항상 string 타입인 name이고, pagesrhymes는 있을 수도 있고 없을 수도 있다.


4.3.2 명시된 객체 타입 유니언 & 4.3.3 객체 타입 내로잉

  • 객체 타입의 조합을 명시하면 객체 타입을 더 명확히 정의할 수 있다.
  • 값이 타입이 객체 타입인 유니언이라면 일반적으로는 모든 유니언에 존재한느 값만 사용할 수 있다.
  • 타입 내로잉을 통해서 타입을 좁혀서 좀 더 정확하게 타입을 추론하고 제어할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type Someone = {
  name: string;
  age: number;
};
type Stranger = {
  name: string;
  marriage: boolean;
};
let person: Someone | Stranger;
person = Math.random() > 0.5 ? { name: "leekoby", age: 33 } : { name: "kimcuby", marriage: true };
person.name;
person.age;
// ❌ Error: Property 'age' does not exist on type 'Someone | Stranger'.
//           Property 'age' does not exist on type 'Stranger'.

person.marriage;
// ❌ Error: Property 'marriage' does not exist on type 'Someone | Stranger'.
//           Property 'marriage' does not exist on type 'Someone'.
if ("age" in person) {
  person.age;
} else {
  person.marriage;
  • 잠재적으로 존재하지 않는 객체의 멤버에 대한 접근을 제한하면 코드의 안전을 지킬 수 있다.
  • 값이 여러 타입 중 하나일 경우, 모든 타입에 존재하지 않는 속성이 객체에 존재할 거라 보장 X
  • 모든 타입에 존재하지 않은 속성에 접근하기 위해 타입을 좁혀야 하는 것처럼 객체 타입 유니언도 타입을 좁혀야한다.

  • 타입 검사기가 유니언 타입 값에 특정 속성이 포함된 경우에만 코드 영역을 실행할 수 있음을 알게 되면, 값의 타입을 해당 속성을 포함하는 구성 요소로만 좁힌다.

  • 코드에서 객체의 형태를 확인하고 타입 내로잉이 적용된다.

Typescript는 if(poem.pages)와 같은 형식으로 참 여부 확인을 허용하지 않는다.

  • 존재하지 않는 객체의 속성에 접근하려고 시도하면 타입 가드처럼 작동하는 방식으로 사용되더라도 타입 오류로 간주
1
2
3
if(poem.pages) {} 
// ❌ Error : Property 'pages' does not exist on type 'Someone | Stranger'.
//            Property 'pages' does not exist on type 'Someone'. 



4.3.4 판별된 유니언

  • JavascriptTypescript에서 유니언 타입으로 된 객체의 또 다른 인기 있는 형태는 객체의 속성이 객체의 형태를 나타내도록 하는 것, 이러한 형태를 판별된 유니언 이라고 한다.

  • 객체의 타입을 가리키는 속성이 판별 값

  • Typescript는 코드에서 판별 속성을 사용해 타입 내로잉 수행

  • 판별된 유니온을 이용해서 타입 내로잉을 하는 것이 좋다.

  • 판별된 유니온이란 각 객체마다 각자의 태그를 붙여서 해당 태그로 타입을 구분할 수 있도록 판별하는 것을 의미

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
type PoemPage = {
    name: string,
    pages: number
    type: "pages"
}

type PoemRhymes = {
    name: string;
    rhymes: boolean;
    type: "rhymes"
}
type Poem = PoemPage | PoemRhymes;
const poem:Poem = Math.random() > 0.5
    ? {name: "leekoby", pages: 9, type: "pages"}
    : {name: "kimcube", rhymes: true, type: "rhymes"}

if(poem.type === "pages"){
    console.log(`${poem.pages}`)
}else{
    console.log(`${poem.rhymes}`)
}

poem.type; // type : "pages" | "rhymes"
poem.pages; 
// ❌ Error : Property 'pages' does not exist on type 'Poem'.
//         Property 'pages' does not exist on type 'PoemRhymes'.


📖 4.4 교차 타입

인터섹션(&)을 이용하여 각 타입에 선언된 모든 속성이 합집합인 타입이 된다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type Stranger1 = {
  name: string;
  age: number;
};
type Stranger2 = {
  name: string;
  marriage: boolean;
};

declare const person: Stranger1 & Stranger2;

/*
person ={
    name: string,
    age: number,
    marriage: boolean,
}
*/

person.name;
person.age;
person.marriage;


교차 타입과 판별된 유니온을 같이 사용한 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type Stranger1 = {
  age: number;
  type: "stranger1"
};
type Stranger2 = {
  marriage: boolean;
  type: "stranger2"
};

declare const person: { name: string } & ( Stranger1 | Stranger2 );

// "name: string"을 가지면서 "Stranger1"인 경우
if(person.type === "stranger1")  {
  person.age;
  person.name;
}
// "name: string"을 가지면서 "Stranger2"인 경우
else {
  person.name;
  person.marriage;
}


4.4.1 교차 타입의 위험성

교차 타입은 유용한 개념이지만, 혼동을 가져오기 쉽기 때문에 가능한 코드를 간결하게 유지해야한다.

🚨 교차 타입을 잘못 사용하면 불가능한 타입인 never가 생성될 위험이 있다.

  • 교차 타입은 잘못 사용하기 쉽고 사용 불가능한 타입을 생성한다.

  • 원시 값은 동시에 여러 타입이 될 수 없기에 교차 타입의 구성 요소로 함께 결합할 수 없다.

  • 두 개의 원시 타입을 함께 시도하면 never 키워드로 표시되는 never 타입이 된다.

  • never는 프로그래밍 언어에서 bottom 타입 또는 empty 타입을 뜻 한다.

  • bottom 타입은 값을 가질 수 없고 참조할 수 없는 타입

1
2
// type SthType = never
type SthType = number & string;

대부분의 Typescript 프로젝트에서는 never 타입을 거의 사용하지 않지만 불가능한 상태를 나타내기 위해 가끔 사용한다.


📚 레퍼런스

Goldberg, et al. 러닝 타입스크립트 / 조시 골드버그 지음 ; 고승원 옮김, 2023.

Typescript 핸드북

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.