TypeScript :: 3. 객체와 타입

김진호·2022년 1월 20일
0

TypeScript

목록 보기
1/6
post-thumbnail

타입스크립트 프로그래밍 책을 보며 공부중입니다.

03-2 객체와 인터페이스

타입스크립트의 타입 계층도에서 object 타입은 인터페이스와 클래스의 상위 타입이다.
object 타입으로 선언된 변수는 number, boolean, string 타입의 값을 가질 수는 없지만, 다음처럼 속성 이름이 다른 객체를 모두 자유롭게 담을 수 있다.

let o : object = {name:'Jack', age:32};
o = { first:1, second:2};

이 코드에서 object 타입은 마치 객체를 대상으로 하는 any 타입처럼 동작 한다. 타입스크립트의 인터페이스 구문은 이렇게 동작하지 않게 하려는 목적으로 고안되었다.
즉, 변수 o에는 항상 name과 age 속성으로 구서된 객체만 가질 수 있게 하여 2행과 같은 코드는 오류가 발생하게 한다.

인터페이스 선언문

타입스크립트는 객체의 타입을 정의할 수 잇게 하는 interface라는 키워드를 제공한다. 인터페이스는 객체의 타입을 정의하는 것이 목적이므로 다음처럼 객체를 의미하는 중활호 {}로 속성과 속성의 타입 주석을 나열하는 형태로 사용한다.

interface 인터페이스 이름 {
  속성이름[?]: 속성타입[,...]
}

다음 코드는 02-2절에서 보았던 IPerson.ts 파일의 내용이다. name과 age 속성을 포함하는 객체의 타입으로 IPerson 인터페이스를 정의하고 있다.

//인터페이스 구문 예
interface IPerson {
  name: string,
  age: number
}

Iperson 인터페이스의 목적은 name과 age라는 이름의 속성이 둘 다 있는 객체만 유효하도록 객체의 타입 범위를 좁히는 데 있다. 따라서 다음처럼 IPerson 인터페이스의 조건을 벗어나는 07~10행 코드는 모두 오류가 발생한다.

//인터페이스의 조건을 벗어나는 예
interface IPerson {
  name: string,
  age: number
}
let good: IPerson = {name: 'Jack', age: 32}

//07행~10행
let bad1: IPerson = {name: 'Jack'} // age 속성이 없으므로 오류
let bad2: IPerson = {age: 32} // name 속성이 없으므로 오류
let bad3: IPerson = {} // name과 age속성이 없으므로 오류
let bad4: IPsrson ={name: 'Jack', age:32, etc: true} // etc 속성이 있어서 오류

선택 속성 구문

인터페이스를 설계 할 때 어떤 속성은 반드시 있어야 하지만, 어떤 속성은 있어도 되고 없어도 되는 형태로 만들고 싶을 때가 있다. 이러한 속성을 선택 속성(optional property) 이라고 한다. 선택 속성은 다음 코드에서 04행처럼 속성 이름 뒤에 물음표 기로를 붙여서 만든다.
etc는 선택 속성이므로 06, 07행은 모두 정상으로 동작한다.

// etc가 선택속성인 IPerson2
interface IPerson2 {
  name: string, // 필수 속성
  age : number, // 필수 속성
  etc? : boolean  // 선택 속성
}
// 06행~07행
let good1 : IPerson2={name 'Jack', age:32}
let good1 : IPerson2={name 'Jack', age:32, etc:true}

익명 인터페이스

타입스크립트는 interface 키워드도 사용하지 않고 인터페이스의 이름으도 없는 인터페이스를 만들 수 있습니다. 이를 익명 인터페이스(anonymous interface) 라고 합니다. 다음 코드에서 변수 ai는 앞에서 선언한 IPerson2 인터페이스와 같은 구성이지만, 익명 인터페이스로 선언 했습니다.

// 익명 인터페이스의 예
let ai:{
  name: string,
  age : number,
  etc?: boolean
} = {name:'Jack', age:32}

익명 인터페이스는 주로 다음처럼 함수를 구현할 때 사용 된다.

// 함수에 사용된 익명 인터페이스 예
 function printMe(me: {name: string, age: number, etc?:boolean}){
   console.log(
     me.etc ?
     	`${me.name} ${me.age} ${me.etc}` :
     	`${me.name} ${me.age}
   )
 }

printMe(ai) // Jack 32

03-3 객체와 클래스

클래스 선언문

타입스크립트는 C++나 자바와 같은 객체지향 언어에서 흔히 볼 수 있는 class, private, public, protected, implements, extend 와 같은 키워드를 제공한다.
문법적인 차이만 약간 있을 뿐 사실상 의미는 다른 언어와 같다.
다음은 클래스 선언문의 기본 형태이다.

class 클래스 이름 {
	[private | protected | public] 속성이름[?]: 속성 타입[...]
}

다음 코드는 name과 age라는 속성을 가진 클래스를 선언한다.

//@ts-nocheck
class Person1 {
    name: string;
    age?: number;
}

다음 코드는 Person12 클래스에 new 연산자를 적용해 jack1이라는 이름의 PErson1 타입 변수를 만든다. jack1은 name과 age 속성을 가지는 타입이므로 06행과 같은 코드를 작성할 수 있다.

// 위 코드에 이어서
let jack1: Person1 = new Person1()
jack1.name = 'Jack'; jack1.age = 32;
console.log(jack1);

접근 제한자

클래스의 속성은 public, private, protect와 같은 접근 제한자(access modifier)를 이름앞에 붙일 수 있다. 만약 생략하면 기본값인 public으로 간주한다.

생성자

타입스크립트 클래스는 constructor라는 이름의 특별한 메서드를 포함한다. 이를 생성자(constructor)라고 한다. (자바스크립트에도 있다)
다른 언어와 다르게 타입스크립트 클래스는 다음 코드에서 02행과 같은 형태로 클래스 속성(name, age)을 선언할 수 있다. 즉, 앞에서 작성한 PErson1 클래스와 당므의 Person2 클래스는 똑같이 동작한다.

class Person2 {
  constructor(public name: string, public age?: number){}
}
let jack2: Person2 = new Person2('Jack', 32);
console.log(jack2)

타입스크립트는 생성자의 매개변수에 public와 같은 접근 제한자를 붙이면 해당 매개변수의 이름을 가진 속성이 클래스에 선언된 것처럼 동작한다. 즉, Person2 클래스는 다음 Person3 클래스처럼 장황하게 구현된 코드를 함축해서 구현한 것이다.

class Person3 {
  name: string;
  age?: number;
  constructor(name: string, age?:number){
    this.name = name; this.age = age;
  }
}
let jack3 : Person3 = new Person3('Jack3', 32)
console.log(jack3)

인터페이스 구현

다른 객체지향 언어와 마찬가지로 타입스크립트 클래스는 인터페이스를 구현할 수 있다.
클래스가 인터페이스를 구현할 때는 다음처럼 implements 키워드를 사용한다.

class 클래스 이름 implements 인터페이스 이름{}

다음 코드는 IPerson4라는 이름의 인터페이스를 구현하는 예이다. 여기서 한 가지 앞으로 기억해 둬야 할 점은 인터페이스는 이러이러한 속성이 있다는 규약(spec) 에 불과할 뿐 물리적으로 해당 속성을 만들지 않는 다는 점이다. 따라서 클래스 몸통에는 반드시 인터페이스가 정의하고 있는 속성을 멤버 속성으로 포함해야 한다.

// 인터페이스의 구현 예
interface IPerson4 {
  name : string,
  age? : number
}

class Person4 implements IPerson4{
  name: string,
  age?: number

다음 코드는 앞서 본 Person2 구현 방식(생성자와 public 키워드 사용)을 인터페이스 구현에 응용한 것이다.

interface IPerson4{
  name:string;
  age?:number;
}
class Person4 implements IPerson4{
  constructor(public name:string, public age?:number){}
}
let jack4 : IPerson4 = new Person4('jack',32);
console.log(jack4)

추상 클래스

타입스크립트는 다른 언어처럼 abstract 키워드를 사용해 추상 클래스를 만들 수 있다.
추상 클래스는 다음처럼 abstract 키워드를 class 키워드 앞에 만든다. 추상 클래스는 자신의 속성이나 메서드 앞에 abstract를 붙여 나를 상속하는 다른 클래스에서 이 속성이나 메서드를 구현하게 한다.

abstract class 클래스 이름{
  abstract 속성 이름: 속성 타입;
  abstract 메서드 이름 (){}
}

다음 AbstractPerson5는 name 속성 앞에 abstract가 붙었으므로 new 연산자를 적용해 객체를 만들 수 없다.

// 추상 클래스 예
abstract class AbstractPerson5{
  abstract name: string
  constructor(public age?:number){}
}

클래스의 상속

객체지향 언어는 부모 클래스를 상속받는 상속 클래스를 만들 수 있는데, 타입스크립트는 다음처럼 extends 키워드를 사용해 상속 클래스를 만든다.

class 상속 클래스 extends 부모 클래스 {...}

다음 Person5 클래스는 AbstractPerson5 추상 클래스를 상속해 AbstractPerson5가 구현한 age를 얻고, AbstractPerson5를 상속받는 클래스가 구현해야 할 name 속성을 구현한다.
타입스크립트에서는 부모 클래스의 생성자를 super 키워드로 호출 할 수 있다.

// ...생략
class Person5 extends APerson5{
  constructor(public name: string, public age?:number){
    super(age)
  }
}
let jack5 : Person5 = new Person5('Jack',  32);
console.log(jack5) // Person5 {name: 'Jack', age: 32}

static 속성

다른 객체지향 언어처럼 타입스크립트 클래스는 정적인 속성을 가질 수 있다. 클래스의 정적 속성은 다음과 같은 형태로 선언한다.

class 클래스 이름{
  static 정적 속성 이름: 속성 타입
}

다음 코드의 클래스 A는 initValue라는 정적 속성을 가집니다. 클래스의 정적 속성은 05행처럼 '클래스 이름. 정적 속성 이름' 형태의 점 표기법을 사용해 값을 얻거나 설정한다.

class A{
  static initValue=1;
}
let initVal = A.initValue // 1

03-4 객체의 비구조화 할당문

다음 코드는 name과 age라는 단어가 각기 다른 의미로 사용되므로 personName, companyName처럼 이 둘을 구분하고 있다.

// 구조화가 필요한 코드 예
let personName ='Jack';
let personAge = 32;

let companyName ='Apple Company, Inc';
let companyAge = 43;

이런식으로 코드를 구현하면 번거롭고 기능확장이 어렵다. 따라서 다음처럼 인터페이스나 클래스를 사용하여 관련된 정보를 묶어 새로운 타입으로 표현하는것을 구조화(structuring) 라고한다.

//Iperson_ICompany.ts
export interface IPerson {
  name: string,
  age : number,
}
export interface ICompany {
  name: string,
  age : number,
}

코드를 이처럼 주고화 하면 다음 코드에서 보듯 jack이나 apple은 물론 jane이나 ms와 같은 비슷한 유형의 변수를 쉽게 만들 수 있다. 이로써 코드이 기능확장이 쉬워진다.

import {IPerson, ICompany} from './IPerson_ICompany'

let jack: IPerson = {name: 'Jack', age:32}, 
    jane: Iperson = {name: 'jane', age:32}

let apple: ICompany = {name:'Apple Computer, Inc', age:43},
    ms: ICompany ={name:"Microsoft", age:44}

비 구조화란?

구조화된 데이터는 어떤 시점에서 데이트의 일부만 사용할 때가 있다. 다음 코드는 구조화된 jack변수에서 jack이 아닌 jack.neme, jack.age 부분을 각각 name과 age 변수에 저장한다. 이 시점부터는 jack 변수는 더 사용하지 않고 그 대신 name과 age 변수만 사용한다. 이처럼 구조화된 데이터를 분해하는 것을 비 구조화 라고 한다.

let name = jack.name, age = jack.age;

비 구조화 할당

비구조화 할당(destructuring)은 ESNext 자바스크립트의 구문으로 타입스크립트에서도 사용할 수 있습니다. 비구조화 할당은 객체와 더불어 05장에서 설명하는 배열과 튜플에도 적용할 수 있습니다. 비 구조화 할당을 객체에 적용하려면 얻고 싶은 속성을 중괄호로 묶습니다.

let {name, age} = jack;

영어 destructuring을 '비구조화'가 아닌 '비구조화 할당'으로 번역한 이유는 다음 코드의 04행에서 보듯 name과 age 변수가 새롭게 만들어지고, name 변수는 jack.name의 값, age 변수는 jack.age의 값을 각각 초깃값으로 할당받기 때문입니다.

import {IPerson} from './IPerson_ICompany'

let jack: IPerson = {name:'Jack', age: 32}
let {name, age} = jack
console.log(name, age) // Jack 32

잔여 연산자

ESNext 자바스크립트와 타입스크립트는 점을 연이어 사용하는 ...연산자를 제공합니다.
이 연산자는 사용되는 위치에 따라 잔여 연산자(rest operator) 혹은 전개 연산자(sperad operator)라고 부른다.
다음 코드에서 address 변수는 5개의 속성을 가지고있다. 이중 country와 city를 제외한 나머지 속성을 별도의 detail이라는 변수에 저장하고 싶다면, 08행에서 보듯 detail 변수 앞에 잔여 연산자를 붙인다.

// 잔여 연산자 예
// rest.ts
let adress: any={
  contry:'Korea',
  cirt:'Seoul',
  address1: 'Gangnam-gu',
  address2: 'Sinsa-dong 123-456',
  address3: '789 street, 2 Floor ABC building'
}
const {country, city, ...detail} = address
console.log(detail)

// 실행결과
// { address1: 'Gangnam-gu',
//   address2: 'Sinsa-dong 123-456',
//   address3: '789 street, 2 Floor ABC building' 
// }

실행 결과를 보면 detail 변수에 cuntry와 city를 제외한 나머지 속성이 담겨 있는 것을 확인 할 수 있다.

전개 연산자

다음 코드에서 01행은 두 객체 앞에 모두 점 3개가 붙었다. 그런데 주의 깊게 보면 이 코드는 앞서 설명한 비구조화 할당문이 아님을 알 수 있다. 점 3개 연산자가 비구조화 할당문이 아닌 곳에서 사용 될 때 이를 전개 연산자라고 한다.

let coord={...{x:0}, ...{y:0}}
console.log(coord) // {x:0, y:0}

전개 연산자는 의미 그대로 객체의 모든 속성을 '전개'하여 새로운 객체로 만들어 준다. 다음 코드는 01행에 part1,part2,part3이라는 세 개의 객체가 선언되었다. 만일, 이를 모두 통합(merge)한 새로운 객체를 만들고 싶다면 03행 처럼 전개 연산자를 사용한다.

let part1={name:'jane'}, part2={age:22}, part3={city:'Seoul', countrt:'Kr'};
let merged = {...part1,...part2,...part3}
console.log(merged)
// 실행결과
// {name:'jane', age:22, city:'Seoul', country: 'Kr'}

03-5 객체의 타입 변환

타입 변환

타입이 있는 언어들은 특정 타입의 변숫값을 다른 타입의 값으로 변환할 수 있는 기능을 제공한다.
이를 타입 변환(type conversion) 이라고 한다.
아래 코드에서 타입 변환이 필요한 예를 보여준다. person 변수의 타입은 object이며 object타입은 name 속성을 가지지 않으므로 오류가 발생한다.

let person: object = {name:'Jack', age:32}
person. name // obejct 타입에 name 속성이 없어 오류 발생

다음은 이 오류를 타입 변환 구문을 사용하여 해결하였다. person 변수를 일시적으로 name 속성이 있는 타입, 즉 {name:string} 타입으로 변환하여 person,name 속성값을 얻게 하였다.

let person: object = {name:'Jack', age:32}
(<{name:string}>person).name // 타입변환 구문으로 오류 해결

타입 단언

타입스크립트는 독특하게 타입 변환이 아닌 타입 단언(type assertion) 이라는 용어를 사용한다. 타입 단언문은 다음 두 가지 형태가 있다.

(<타입>객체)
(객체 as 타입)

두가지 모두 ES5 자바스크립트 문구가 아니다. 따라서 자바스크립트의 타입 변환 문구와 구분하기 위하여 타입 단언이라는 용어를 사용한다.
타입 단언문의 예를 설명하기 전에 코드의 의미를 명확하게 하고자 다음과 같은 인터페이스를 구현한 INameable.ts 파일을 만들어보자.

export default interface INameable{
	name : stirng
}

아래 코드는 단일 타입 단언의 두 가지 형태를 볼 수 있도록 작성하였다. 02행의 object 타입 체 obj는 04행이나 05행의 코드 형태로 INameable 타입 객체로 변환되어 자신에게 담긴 객체의 name 속성값을 얻는다.

import INameable from './INameable'
let obj: object = {name:'Jack'}

let name1= (<INameable>obj).name;
let name2 = (obj as INameable).name;
console.log(name1, name2); // jack, jack

이처럼 타입 단언의 두 가지 구문은 서로 형태만 다를 뿐 내용상으로는 같다.

profile
느린 걸음도 먼 길을 갈 수 있다.

0개의 댓글