인터페이스와 타입
인터페이스 소개
인터페이스는 객체의 구조를 정의하는데 사용되는 타입이다, 객체의 껍데기 또는 설계도라고 할 수 있다
다음과 같은 범주에서 타입 정의
- 객체의 스펙 (속성과 속성 타입)
- 함수의 파라미터
- 함수의 스펙 (파라미터 타입, 반환 타입)
- 배열과 객체를 접근하는 방식 (인덱스 타입) ← 이거 좀 신기
- 클래스
기존 타입별칭과 똑같은거 아닌가??
| type | interface |
|---|---|
| 기존타입 또는 새 타입을 생성하는데 사용 | 객체타입의 구조를 정의하는데 사용 |
| 다른 타입 또는 인터페이스를 상속하거나 구현할 수 없음 | 다른 인터페이스를 상속하거나 구현할 수 있음 |
| 리터럴 타입, 유니온타입, 인터섹션 타입을 사용할 수 있음 | extends 키워드로 인터페이스 확장이 가능 |
| 간단한 타입 별칭을 생성할 때 적합 | 잘 정의된 구조의 객체 타입을 정의할 때 적합 |
Type 방식과 Interface방식 모두, 타입을 지정할 때 사용할 수 있고 Interface는 객체 타입의 구조를 정의하는데만 사용된다 논쟁거리는 여기에 있다. Type 방식은 기존타입 그리고 새로운 타입을 생성하는데 사용할 수 있고, 마찬가지로 객체타입 구조를 정의하는데도 사용할 수 있다 ** 그렇다면 객체타입을 정할 때,Type Alias 방식과 Interface 방식, 둘 중 어떤 것을 사용해야 할까?
인터페이스를 사용하는 가장 큰 장점은 확장 가능성이다. extends 명령어를 통해 인터페이스를 확장(상속) 할 수 있고, 같은 인터페이스끼리 호환(선언 병합) 가능하기 때문에 유연하게 타입 지정을 할 수 있다
````ad-white
title: 인터페이스 호환
```ts
interface Shape {
color: string;
}
interface Shape {
radius: number;
}
const myCircle: Shape = {
color: 'red',
radius: 10,
}
```
인터페이스는 같은 인터페이스에 다른 속성들을 추가하면 자동으로 해당 인터페이스에 **속성들을 병합**시킨다
````
````ad-white
title:인터페이스 확장
```ts
interface Shape {
color: string;
}
interface Circle extends Shape {
radius: number;
}
const myCircle: Circle = {
color: 'red',
radius: 10,
}
```
`extends`를 사용하여 **인터페이스를 확장(상속)** 할 수 있다
다중 상속 또한 지원한다
````title: **Type Alias** vs **Interface**
타입스크립트 팀은 **개방-폐쇄 원칙**에 따라 확장성이 열려있는 JavaScript 객체의 동작 방식과 비슷하게 연결하도록 **Interface를 개발**했다
따라서 가능한 Type Alias 보다 **Interface 사용을 권장**하고,
**유니온 타입**, **튜플 타입**을 반드시 사용해야 하는 상황이면 **Type Alias를 사용**하도록 권장하고 있다
그리고 **대부분의 타입스크립트 라이브러리**가 **Interface로 타입 선언**을 하기 때문에 **코드 일관성이나, 확장성 측면**에서 Interface를 주로 사용하는 사람들이 더 많다인터페이스 기본 문법
선택적 속성 그리고 타입가드
선택적 속성
이전에 한번 소개한 적이 있는 녀석(?) 이다.
말 그대로 객체 속성에 대한 타입지정을 할 때, 옵션으로 넣어도 되고 넣지 않아도 되는 속성에 대한 타입지정을 할수 있다
````ad-green
title: 선택적 속성
```ts
interface User {
name: string;
age?: number
}
const myUser: User = {
name: "JeongHoon",
age: 24,
}
```
위와 같이 age속성을 선택적 프로퍼티로 만들면 age속성을 넣어도 되고 넣지 않아도 되는 **옵션으로써 사용**할 수 있다
````
````ad-red
title:에러
```ts
interface User {
name: string;
age?: number
}
const myUser: User = {
name: "JeongHoon",
age: 24,
}
if (myUser.age > 20) { // 에러 발생
console.log("Adult");
}
```
````
Object is possibly 'undefined'
선택 속성을 가지고 그대로 로직에 들고와 코드를 작성하게 되면 해당 속성이 확실히 사용되는지 안되는지 알 수 없기 때문에 에러를 발생시킨다
따라서 선택 속성을 사용할 때에는 타입가드 기법을 잘 활용하여 코드를 작성해야 한다
title: 수정된 버전
```ts
interface User {
name: string;
age?: number
}
const myUser: User = {
name: "JeongHoon",
age: 24,
}
if (myUser.age && myUser.age > 20) { // 타입 가드 적용
console.log("Adult");
}
```타입가드
title: 페이지 이동
타입가드 페이지로 이동 → [[(타입 스크립트) 타입가드]]읽기전용 (ReadOnly)
읽기 전용 속성 (ReadOnly Property) 는 말 그대로, 인터페이스로 객체를 처음 생성할 때만 값을 할당하고, 그 이후로는 변경할 수 없는 속성을 의미한다
다음과 같이 readonly 만 붙여주면 된다
interface User {
name: string;
age: number;
gender?: string;
readonly birthYear: number;
}
const user: User = {
name: "JeongHoon",
age: 24,
birthYear: 2000,
}
user.birthYear = 2004; // 에러 발생title: const vs readonly
가만보면 `const`도 한번 할당해주고 나면 바꿀수 없는데, readonly와 거의 똑같아 보이는데, **어떤 점이 다를까?**
결론은 거의 똑같다 이다, 단지 const 는 변수할당에 사용되고, readonly는 속성에서 사용된다는 것만 기억하면된다
실제로 객체에 `as const`와 같이 *타입단언*[^1]을 하면 전부 readonly로 바뀌는것을 볼 수 있다
[^1]: `as <타입>`으로 사용되며 컴파일러에게 타입을 명시적을 알리는 방법
ReadOnly 활용
만약 모든 속성이 readonly 라면 일일이 전부 readonly를 적지 않고, 유틸리티(Utility) 나 단언(Assertion) 을 통해 구현할 수 있다.
````ad-white
title: 무식하게
```ts
interface User {
readonly name: string;
readonly age: number;
}
const user: User = {
name: "JeongHoon",
age: 24
}
user.age = 20; // 에러
```
````
````ad-white
title: Utility
```ts
interface User {
name: string;
age: number;
}
const user: Readonly<User> = {
name: "JeongHoon",
age: 24
}
user.age = 20; // 에러
```
````
````ad-white
title: Assertion
```ts
const user = {
name: "JeongHoon",
age: 24
} as const
user.age = 20; // 에러
```
인터페이스를 사용하지 않고, `as const` 를 사용하면 이 자체가 리터럴 타입을 가지게 되고
IDE에서 에러를 잡아준다
같은조건으로 인터페이스를 사용했을 때
readonly이므로 값을 변경하면 에러가 발생하긴하지만, **IDE에서 잡아주지 못한다**
````리터럴 타입: 쉽게말해 변하지 않는 값, 또는 그자체로 타입인 값
읽기전용 배열
const arr: ReadonlyArray<number> = [1, 2, 3, 4];
// 에러
arr[2] = 1; // 'readonly number[]' 형식의 인덱스 시그니처는 읽기만 허용됩니다.
arr[5] = 1; // 'readonly number[]' 형식의 인덱스 시그니처는 읽기만 허용됩니다.
arr.splice(0, 1); // 'splice' 속성이 'readonly number[]' 형식에 없습니다.
arr.push(4); // 'splice' 속성이 'readonly number[]' 형식에 없습니다.인터페이스 호환, 확장
<위 설명 참고>
인터페이스 함수 타입
여기서부터는 조금 생소하다, 하지만 이전에 type alias에서 조금 다룬적 있다
시그니처란 시그니처(Signature)는 함수나 메서드가 어떤 인자를 받고 어떤 타입의 값을 반환하는지를 명시한 것을 의미한다. 간단히 말해, 함수나 메서드의 “형태”를 정의하는 것이다. 함수 시그니처는 함수의 입력(매개변수)과 출력(반환 타입)을 포함한다
호출 시그니처
interface Login {
(userName: string, password: string): boolean; // 타입스크립트는 함수명이 아닌, 함수의 모양을 인터페이스에 쓴다
}
// 매개변수 이름이 인터페이스와 일치할 필요 없다
const loginUser: Login = function(id, pw) {
console.log('로그인 했습니다');
return true;
}자바스크립트에서는 함수도 속성값을 가질수 있다
// 함수 선언
function greet(name) {
return `Hello, ${name}!`;
}
// 함수에 속성 추가
greet.language = 'English';
greet.version = 1.0;따라서 인터페이스 또한 함수타입을 정의할때 다음과 같이 함수 속성 값을 정의할 수 있다
interface Login {
(userName: string, password: string): boolean; // 타입스크립트는 함수명이 아닌, 함수의 모양을 인터페이스에 쓴다
totalCall?: number; // 함수 속성
}
// 매개변수 이름이 인터페이스와 일치할 필요 없다
const loginUser: Login = function (id, pw) {
if (loginUser.totalCall !== undefined) {
if (loginUser.totalCall >= 0) {
loginUser.totalCall++;
console.log(`${loginUser.totalCall}번 로그인 했습니다`);
return true;
}
} else {
loginUser.totalCall = 0; // 타입가드를 통해 체크한후 함수 속성값 정의
}
return false;
};
loginUser('leokang', '123');
loginUser('leokang', '123');호출 시그니처는 특히 자바스크립트의 객체의 prototype에 메소드를 추가할 때 유용하게 사용된다
interface Object {
getShortKeys(this: object): string[];
}
Object.prototype.getShortKeys = function() {
return Object.keys(this).filter((key) => key.length <= 3);
}
const obj = {
a: 1,
bb: 2,
ccc: 3,
dddd: 4,
}
console.log(obj.getShortKeys());prototype에 메소드를 추가하는 상황은 그래도 왜 호출시그니처를 사용하는지는 알겠는데, 그 외의 상황들에선 왜 저렇게 까지 해야할까 싶긴하다
아마 그 고민은 클래스에서 인터페이스를 활용할 때 풀릴 듯 싶다
함수 오버로딩
interface Add {
(x: number, y: number): number;
(x: string, y: string): string;
}
const add: Add = function(x: any, y: any): any {
return x + y;
}인터페이스를 활용하여 함수 오버로딩을 할수 있다
문득 기존에는 어떻게 함수 오버로딩을 했는지 궁금해져서 찾아봤는데 함수 시그니처를 미리 선언해두는 방식으로 오버로딩을 할 수 있다고 한다
function add(x: number, y: number): number;
function add(x: string, y: string): string;
function add(x: any, y: any): any {
return x + y;
}인터페이스를 활용한 방법과 매우 유사하다!
인터페이스 클래스 타입 (Implements)
타입스크립트에서도 다른 언어들과 마찬가지로 클래스가 일정 조건을 만족하도록 타입규칙을 정할 수 있다 따라서 인터페이스에서 정의한 속성 및 메소드가 클래스에 모두 포함되어있어야 한다.
하지만 이것은 인터페이스가 클래스가 갖추어야 할 최소한의 구조만 정의한 것일 뿐, 클래스가 추가적인 속성이나 메소드를 가지는 것은 문제가 되지 않는다
interface Animal {
name: string;
sound(): void;
}
// interface에서 타입 명시한것들은 타입지정을 안해줘도 되는거아니야?? 할수도 있지만,
// 클래스에서는
class Dog implements Animal {
name: string;
breed: string; // 추가 속성
constructor(name: string, breed: string) {
this.name = name;
this.breed = breed;
}
sound() {
console.log('Woof!');
}
fetch() {
console.log(`${this.name} is fetching!`);
}
}
const myDog = new Dog('Rex', 'Golden Retriever');
console.log(myDog.name); // "Rex"
console.log(myDog.breed); // "Golden Retriever"
myDog.sound(); // "Woof!"
myDog.fetch(); // "Rex is fetching!"구성 시그니처 (Construct Signiture)
위의 호출 시그니처는 함수 타입 구조를 정의하는 것이라면, 구성 시그니처는 new 클래스() 생성자 함수 타입 구조를 정의하는 것이다
이를 응용해 함수 매개변수를 클래스로 받아 대신 초기화를 할 수 있다
interface CatConstructor {
new (name: string): Cat;
}
class Cat {
public voice:string = "meow~"; // 당연히 이것도 가능
// public 접근제어자
// name이 자동으로 클래스 속성으로 선언되고, this.name = name;이 실행됩니다.
constructor(public name: string) {}
}
function makeKitten(c: CatConstructor, n: string) {
return new c(n);
}
const kitten = makeKitten(Cat, "Lucy");
console.log(kitten.name);kitten.name은 할당된적이 없는데 에러가 나지 않아서 놀랐다 찾아보니 타입스크립트에는 public, protected, private이라는 접근 제어자가 존재하고, 기본적으로 멤버변수들의 접근 제어자는 public이라고 한다.
생성자에서만 파라미터에 접근제어자를 썼을 떄 자동으로 클래스 속성으로 할당할 수 있다고 한다.
인터페이스 인덱스 타입
지금까지 사용한 인터페이스는 직접 속성의 타입을 하나하나 지정해주어 사용했다
인덱스 타입을 사용하면, 일일이 정해주지 않고, 조금 더 유기적으로 타입을 지정할 수 있다.
쉽게 말해서 [변수: 타입1]: 타입2 꼴은 타입1의 꼴로 조회를 했을떄 나오는 반환값의 타입이 타입2 라고 명시 하는것이다
````ad-white
title: 객체 적용
```ts
type Score = 'A' | 'B' | 'C' | 'D' | 'F';
interface User {
name: string;
[grade: number]: Score; // indexable 타입 (선택적 프로퍼티 처럼 반드시 선언 안해줘도 된다.)
}
const user1: User = {
name: '홍길동',
1: 'A',
};
const user2: User = {
name: '임꺾정',
3: 'F',
};
```
**Index 타입은 선택적 속성처럼** 반드시 선언해주지 않아도 된다
````
````ad-white
title: 배열 적용
```ts
interface IItem {
[itemIndex: number]: string | boolean | number[] // 여러개의 타입을 유니온
}
let item: IItem = ['Hello', false, [1, 2, 3]];
console.log(item[0]); // Hello
console.log(item[1]); // false
console.log(item[2]); // [1, 2, 3]
```
객체 뿐아니라 배열도 인덱서블 타입 지정을 해줄 수 있다.
즉 개발자가 **배열또는 객체**를 **조회, 반환** 하는 **타입을 정해 줄수 있다**는 의미이다
````이를 활용하면 인덱서블 타입을 사용하여, 인터페이스에 정의되지 않은 속성들을 유기적으로 사용할 수 있다는 장점있다 (마치 자바스크립트 처럼)
interface IUser {
name: string;
age: number;
[userProp: string]: string | number;
}
let user: IUser = {
name: "Leo",
age: 24,
// 인터페이스에 정의되지 않은 속성
// 인데서블 타입에만 맞으면 추가가능
email: 'leokang123@naver.com',
gender: 'M',
}
console.log(user);