객체

Kaia·2023년 8월 16일

typescript

목록 보기
4/4
post-thumbnail
💡 복잡한 객체 형태를 설명하는 방법과 타입스크립트가 객체의 할당가능성을 확인하는 방법에 대해 알아보자

객체 리터럴

각자의 타입이 있는 키와 값의 집합

{…} 구문을 사용해서 객체 리터럴을 생성하면 해당 속성 기반으로 객체 타입을 유추한다.

{a: 1, b: 2}
// 객체 값과 동일한 속성명과 원시 타입을 가진다.
{a: number, b: number}
const poet = {
	name: 'Mary',
	born: 1995
}
// 값의 속성에 접근
poet.name; // 타입 string
poet[born]; // 타입 number

// 다른 속성 이름으로 접근하면 에러
poet.end;

객체 타입 선언

기존 객체에서 타입을 유추하는 것 대신 객체의 타입을 명시적으로 선언

let poet = {
	name: string;
	born: number;
}

poet = {
	name: 'Mary',
	born: 1995
}

별칭 객체 타입

객체의 타입에 별칭을 할당해서 사용하는 방법

일반적으로 타입스크립트 프로젝트는 객체 타입을 설명할 때 인터페이스 키워드를 사용하는 것을 선호하며 별칭객체타입과 인터페이스는 거의 동일

interface Poet {
	name: string;
	born: number;
}

let poetLater: Poet;
poetLater = {
	name: 'Mary',
	born: 1995
}

타입스크립트는 객체 타입을 체크할 때 좀 더 엄밀한 속성 검사를 진행하게 된다.

구조적 타이핑

TS 타입시스템은 구조적으로 타입화되어 있다. 타입을 충족한 모든 값을 해당 타입의 값으로 사용할 수 있음

즉 매개 변수나 변수가 특정 객체타입으로 선언되면, 타입스크립트에 어떤 객체를 사용하든 해당 속성이 있어야 한다.( 집합관계를 취한다)

위 정의가 이해가 안될 수 있다. 코드로 이해해보자!

interface A {
  id : number
}

interface B {
  id : number 
}

let a : A;

let b : B = {
  id: 5,
}

a = b //OK

a 변수는 A 타입으로 선언 되었지만 ‘구조적으로 동일’한 B 타입의 객체를 할당 하더라도 에러를 내지 않음

interface A {
  id : number
}

interface B {
  id : number 
  name : string 
}

let a : A;

let b : B = {
  id: 5,
  name: 'june'
}

a = b // OK

B 타입에는 A 타입에 없는 name 속성이 있지만 해당 코드도 통과한다.

  • 구조적 타입화 ⇒ 집합관계로 이해
  • A타입 속성에 존재하는 값이 B타입에 속성으로 존재하는가? YES
interface A {
  id : number
}

const a : A = {
  id: 1,
  name: 'a' // Error
}

사용 검사

객체 타입으로 애너테이션된 위치에 값을 제공할 때, 할당하는 값에는 객체 타입의 필수 값이 있는지, 타입이 맞는지 검사하는 것

interface Name {
	first: string;
	last: string; 
}

const a: Name = {
	first: 'aaa',
	last: 'aaa' 
}

// (1) 두 가지 속성이 모두 없는 객체는 사용할 수 없음
const b: Name = {
     ~~~
	// Error: 'last' is missing / required in Name
  	first: 'aaa',
}

// (2) 일치하지 않는 타입도 허용되지 않음
const b: Name = {
    first: 'aaa',
    last: 1234
}

초과 속성 검사

변수가 객체 타입으로 선언되고, 초기값에 객체타입에서 정의된 것보다 많은 필드가 있으면 오류 발생

interface Poet {
	name: string;
	born: number;
}

const poet: Poet = {
	name: 'Mary',
	born: 1995
}

const ExtraProperty: Poet = {
	activity: 'walking',
	~~~~~~~~~ Error 
	name: 'Mary',
	born: 1995
};
  • 분명 아까는 집합관계면 할당된다고 했는데.. 무언가 이상하다. 아래 코드를 통해 살펴보자.
    interface Avengers {
       name: string;
    }
    
    let hero: Avengers;
    hero = { name: 'Captain', location: 'Pangyo' }; // Err
    • 왜?

      객체 리터럴을 변수에 직접 할당할 때나 인수로 전달할 때, 초과 프로퍼티 검사 (excess property checking)를 받게 된다. 
      
      그래서 만약 객체 리터럴이 "대상 타입 (target type)"이 갖고 있지 않은 프로퍼티를 갖고 있으면, 당연하게도 에러가 발생된다.
      interface Avengers {
         name: string;
      }
      
      /* 
      type Avengers = {
         name: string;
      };
      
      let hero: {
         name: string;
      };
      */
      
      let hero: Avengers;
      // hero = { name: 'Captain', location: 'Pangyo' };
      
      let tmp = { name: 'Captain', location: 'Pangyo' };
      hero = tmp; // OK
      console.log(hero); // { name: 'Captain', location: 'Pangyo' }
    • 왜?

      그러나 객체를 다른 변수에 할당하게 되면 변수 tmp 는 초과 프로퍼티 검사를 받지 않기 때문에(우회하기 때문에), 컴파일러는 에러를 주지 않는 것이다.
      
      직접 초기값을 할당하지 않는경우, 즉 참조에 의한 초기값 할당은 타입 시스템의 초기속성검사에서 제외된다.
      let tmp = { name2: 'Captain', location: 'Pangyo' };
      hero = tmp; // Error
    • 왜?

      그렇다고 해서 다음과 같이 인터페이스에 정의된 공통 객체 프로퍼티가 없으면 초과 프로퍼티 검사 회피든 뭐든 무조건 에러가 발생된다.

즉, 정리하자면

  • 초과 속성 검사는 객체 타입으로 선언된 위치에서 생성되는 객체 리터럴에 대해서만 일어난다.
  • 기존 객체 리터럴을 할당하면 초과속성검사를 우회함
  • 그러나 속성값에 접근은 불가하다.
    interface Poet {
    	name: string;
    	born: number;
    }
    interface IExtraProperty {
    	name: string;
    	born: number;
    	activity: string;
    }
    
    const ExtraProperty: IExtraProperty = {
    	activity: 'walking',
    	name: 'Mary',
    	born: 1995
    }
    
    const poet: Poet = ExtraProperty;
    poet.activity;
    	~~~~~~~~~~
    Property 'activity' does not exist on type 'Poet'.
    
    // 함수로 된 예제
  • 이후에 정리할 배열, 클래스, 함수 등 이후 객체타입과 새로운 객체가 있는 모든 곳에서 초과 속성 검사 실행하게 되는데 그때 가서 다시 보자!

중첩된 객체 타입

객체는 다른 객체의 멤버로 중첩될 수 있으며, 객체 타입도 중첩된 객체 타입을 나타낼 수 있다.

interface Author {
	first: string;
	last: string; 
}

interface Poem {
	name: string;
	author: Author;
}

const poem: Poem = {
	name: 'Mary',
	author: {
		name: 'Sylvia' // Error
	}
}

선택적 속성

타입의 속성 애너테이션 :앞에 ?를 추가하여 선택적 속성임을 나타낼 수 있다.

interface Poem {
	name: string;
	author: undefined;
}

const poem: Poem = {
	name: 'Mary' // OK
}

Property 'author' is missing in type '{ name: string; }' but required in type 'Poem'.(2741)
  • undefined를 포함한 유니언 타입과 헷갈리지 말기! 선택적 속성은 존재하지 않아도 된다. 하지만 필수로 선언된 속성과 | undefined 는 그 값이 undefined 일지라도 반드시 존재해야 함

속성이 조금 다른, 하나 이상의 서로 다른 객체의 타입을 설명할 수 있어야 하고 (유니언)
또한 속성값을 기반으로 타입을 좁혀야 할 수도 있다. (내로잉)

객체 타입 유니언

객체도 원시 타입을 유니언으로 정의하는 것 처럼 동일하게 할 수 있다.

const poem = Math.random() > 0.5
	? {name: 'a', pages: 7}
	: {name: 'b', rhymes: true}
  • 유추된 객체 타입 유니언

여러 타입 중 하나가 될 수 있는 초기값이 주어지면 객체 타입 유니언으로 유추

const poem = Math.random() > 0.5
	? {name: 'a', pages: 7}
	: {name: 'b', rhymes: true}

// 타입
//{
//	name: string;
//	pages: number;
//	rhymes?: undefined; 초기값 없는 선택적 타입이지만 구성요소로 주어짐
//}
//|
//{
//	name: string;
//	pages?: undefined; 초기값 없는 선택적 타입이지만 구성요소로 주어짐
//	rhymes: boolean; 
//}
poem.name; // string
poem.pages; // number | undefined
poem.rhymes; // boolean | undefined

이렇게 되면 undefined인 속성에 접근하여 잠재적인 오류를 발생시킬 수 있는 경우가 발생할 수 있으며

또한 오류 메세지의 가독성 또한 좋지 못하다.

  • 명시적 객체 타입 유니언

잠재적으로 존재하지 않는 객체의 멤버에 대한 접근을 제한함으로써 코드의 안전을 높임

interface PoemWithRhymes {
	name: string;
	rhymes: boolean;
}
interface PoemWithPage {
	name: string;
	pages: number;
}
interface Poem = PoemWithPage; | PoemWithRhymes;

const poem: Poem = Math.random() > 0.5
	? {name: 'a', pages: 7}
	: {name: 'b', rhymes: true};

poem.name; //OK
poem.pages; // Error ->  속성 검사 시 page가 존재한다는 보장 없음
poem.rhymes; // Error ->  속성 검사 시 rhymes가 존재한다는 보장 없음

에러를 통해 없을 수 있는 속성에 대해서 속성 검사가 제대로 이루어짐.

그리고 보장된 속성 값에 접근하기 위해서는 내로잉을 통해 접근해야 한다.

(6) 객체 타입 내로잉

타입 가드를 통해서 타입을 내로잉

(instance of, typeof, use in keyword, discriminated Union etc..)

if (poem.pages){ 
    ~~~~~~~~~~~
		Error: Property 'pages' does not exist on type 'poem'.
	...
}else{
	...
}

// 타입가드가 참여부를 확인하기 전에 존재하지 않는 속성에 접근하려고 시도하면 타입오류로 간주
  • 타입 가드(in 연산자)
if ('pages' in poem){ // 'in' keyword를 이용한 type narrowing
   poem.pages;
}else{
   poem.rhymes;
}
  • 판별된 유니언

객체 타입을 객체 안의 속성으로 저장하여 객체 속성이 객체의 타입을 나타내게 끔 하는 방법

interface PoemWithRhymes {
	name: string;
	rhymes: boolean;
	type: 'rhymes';
}
interface PoemWithPage {
	name: string;
	pages: number;
	type: 'pages';
}
type IPoem = PoemWithPage | PoemWithRhymes;

const poem: IPoem = Math.random() > 0.5
	? {name: 'a', pages: 7, type:'pages'} // type 추가
	: {name: 'b', rhymes: true, type: 'rhymes'}; // type 추가

if (poem.type === 'pages'){  // 판별된 유니온을 활용한 type narrowing
    poem.pages;
}else{
    poem.rhymes;
}

& 교차 타입

기존 객체 타입을 별칭 객체 타입으로 결합해 새로운 타입 생성 합집합

type Common = {
  name: string,
  age: number,
  gender: string
}

type Animal = {
  howl: string
}

type Cat = Common & Animal;
type Dog = Common | Animal;

let dog: Dog = {
  howl: 'dogggg'
}
let cat: Cat = {
  age: 3,
  gender: 'C',
  name: 'CC',
  howl: 'cattttt'
}

교차 타입은 유니언 타입과 결합할 수 있다.

type Name = { id: number } & ({ first: string } | { last: string })

const name: Name = {
    id: 1,
    first: 'a'
}

never

교차 타입은 불가능한 타입을 생성해 낼 수도 있음

  • 예를 들어, 원시 타입의 경우는 교차 타입의 구성요소로 함께 결합될 수 없다. 이 경우 타입은 never로 간주된다.
    type notPossible = number & string;

대부분의 타입스크립트 프로젝트는 never 타입을 거의 사용하지 않지만, 코드에서 불가능한 상태를 나타내기 위해 등장하며 대부분 교차 타입을 잘못 사용해 발생한 실수인 경우가 많음.

0개의 댓글