객체의 구조를 정의하는 타입
- 구현을 강요해서, 의도를 명확히 할 수 있다
- TS에만 있는 개념
- 객체에는 객체, 배열, 함수, 클래스가 포함
⇒ 데이터가 객체일 때 type, interface둘 다 사용해도 상관없지만, interface를 더 권장한다
클래스와 인터페이스
- 안에 속성과 메서드들이 타입과 함께 선언 되어있음(구조만, 당연 값 정의는x)
- 인터페이스 타입의 변수/상수를 선언하고 여기에서 값을 구현(속성은 값, 메서드는 body)
- 쉼표로 구분한다
interface Person { name: string age: number, greet(phrase: string):void } const user1:Person = { name: 'jjong', age: 12, greet(phrase: string) { console.log(phrase+this.name) //this는 user1을 말한다, 그래서 여기서 this.name은 jjong } } user1.greet('Hello')
- 클래스에서도 구현이 가능하다
implements 인터페이스명
으로 구현한다는 것을 표시- 인터페이스에서 명세한 속성과 메서드를 모두 구현해야 한다
- 메서드는 바로 구현, 속성은 선언과 동시에 구현(정의)하거나 생성자로 값을 정의
- 추상과 비슷함. but 추상클래스는 오버라이드할 함수와 메서드가 부분적이고, 정의가 되어 있어도 됨
- 클래스에서 추가로 속성이나 메서드를 선언할 수 있다
- 여러 인터페이스를 구현 가능하다(쉼표로 구현)
- 변수나 상수의 타입을 인터페이스로 지정하고 해당 인터페이스를 구현한 클래스의 인스턴스를 값으로 정의 해도 ok
interface Person { name: string; greet(phrase: string): void } class me implements Person { name: string //하단 생성자에서 값 정의 age = 10 //새로 추가된 속성 constructor(n: string) { this.name = n } greet(phrase: string) { //메서드 구현 console.log(phrase, this.name) } } const An:me = new me("An") An.greet("hello") const Je:Person = new me("Je") //인터페이스 타입이라 하고 클래스의 인스턴스를 값으로 받아도 됨 Je.greet("Hi")
- 인터페이스나 인터페이스를 구현한 클래스로 타입을 지정하면, 다른건 몰라도 여기에 구현된 특정 메서드만은 있다고 확신할 수 있음
- 인터페이스에 접근제어자는 쓸 수 없음
- 구현 클래스에서는 구현한 모든 속성과 메서드는 public으로만 가능
- readonly는 가능
- readonly로 지정하면, 구현한 클래스에서만 값을 정의할 수 있음
- 물음표?도 인터페이스에 적용할 수 있음
- 물음표로 속성이나 메서드를 지정하면, 클래스에서 구현해도 되고 안해도 된다
- 클래스에도 마찬가지로 물음표를 지정할 수 있다 ⇒ 해당 속성이나 메서드는 정의해도 되고 안해도 된다
- 매개변수나 함수에도 물음표를 쓸 수 있다
interface UserI { name: string; readonly age: number; //age의 값은 초기화 이후에 변경할 수 없음 isValid?: boolean //isValid는 구현해도, 안해도 된다 }
- 인터페이스는 인터페이스를 상속받을 수 있다
interface Name { name:string } interface Greet extends Name { //Greet 인터페이스는 name 속성이 포함된다 age: number //Greet 속성에 새로 추가된 속성, 따라서 Greet은 name, age 속성 두개를 갖고 있음 } class Person implements Greet { //name, age 두개 속성을 정의해야함 ... }
- 같은 이름의 인터페이스의 중복선언은 병합을 한다
- 나중에 선언하는 인터페이스에서 기존에 존재하는 속성을 다시 정의한다면 타입은 같아야 한다
interface User { name: string age: number } interface User { //같은 User라는 이름의 인터페이스, 이로 인해 User는 세 속성을 가지게 됨 isValid: boolean, age: string //X, 기존에 있는 인터페이스의 속성과 같은 타입이어야 한다(number) } const user: User{ name: 'me', age: 12, isValid: true }
함수와 인터페이스
⇒ 함수일 땐 type을 더 많이 쓰긴한다
interface 인터페이스이름 { (매개변수: 매개변수타입): 반환타입 //이 꼴을 호출 시그니처라고 부른다 }
interface addFn { (x: number, y: number): number } const getUserName: addFn = (a: number, b: number) => a+b //호출 시그니처의 매개변수 이름과 같지 않아도 됨
인스턴스와 인터페이스
interface UserI { name: string getName(): string } //클래스에서 이 인터페이스를 구현할 수 있다
interface UserC{ new (u: string): UserI //구문(생성) 시그니처 } /* 우리는 생성자를 호출할 때 new User('str') 형태로 호출한다 User('str') => 생성자 함수를 호출하는 것과 같은 형태, => 따라서 호출 시그니처 형태로 매개변수 먼저 작성 => (u: string) new => 인스턴스 생성할 때 붙으므로 호출 시그니처 앞에 붙임 => new (u: string) 반환은 UserI => 클래스가 구현한 인터페이스는 사실 클래스의 타입이 아닌 이 클래스로 생성되는 인스턴스의 타입임 => 따라서 반환값을 인스턴스로 => new (u: string): UserI 호출할 때는 인스턴스로 생성하고 싶은 클래스를 호출함. 따라서 UserC의 타입을 가지는 값으로는 User를 넣으면 됨 */
interface UserI { name: string getName(): string } class User implements UserI { private name constructor(name: string) { //생성자가 없이 초기화 방법 써도 됨. 구문 시그니처와는 상관 없음 this.name = name } getName() { return this.name } } interface UserC{ new (u: string): UserI //반환값인 인터페이스를 구현하는 클래스만 이 타입이 될 수 있음 } function hello(userClass: UserC) { const user = new userClass('jjong') console.log(user.getName()) } hello(User)
인터페이스의 인덱스
인터페이스의 속성의 개수나 이름을 특정하지 않고, 속성의 타입과 값의 타입을 지정하기만 할 수 있다
[prop: 속성타입]: 값타입
(이름이 prop이 아니어도 됨)
- 인덱스와 동일한 타입의 속성을 추가적으로 선언할 수 있다 ⇒ 해당 속성은 필수가 된다
interface Arr { [key: number]: string } const arr:Arr = ['a','b','c'] //사실 배열은 인터페이스보다 Array<string> 형태가 더 일반적 interface ErrorContainer { [prop: string]: string; } const errorBag: ErrorContainer = { id: "ID", gender: "F", 1: "2" //O, 1은 문자열로 변환될 수 있음 age: 12 //X, 값의 타입이 다름 } interface AnimalContainer { [prop: number]: string; 2: string // 필수 속성을 추가로 선언, 인덱스와 동일한 타입이어야 한다 } const errorBag: AnimalContainer = { 1: "tiger", 2: "panda" //필수 속성 pig: "pig" //X, 속성의 타입이 다름 }
- 주의! string 타입과 string literal 타입은 다른 것이다!
- let으로 선언하면 string 타입으로 추론
- const로 선언하면 string literal 타입으로 추론
- Object.keys는 string[]이므로 각 키는 string 타입으로 추론
- 또한 대상 객체의 값은 모르고 타입만 object라고 지정하면, 인덱스 시그니처가 없다고 판단. string타입의 키로 접근할 수 없음
const obj = { foo: "hello", //현재 obj의 키는 string 리터럴 타입(foo)만 존재 } //case 1 let propertyName = "foo" console.log(obj[propertyName]) //X, string 타입 //case2 const propertyName2 = "foo" console.log(obj[propertyName2]) //O, string 리터럴 타입 //case3 Object.keys(obj).map(key => obj[key]) //X, string 타입 //case4 function fn(cls: object) { Object.keys(cls).map(key => cls[key]) //X, string 타입 키로 접근 불가 }
⇒ <해결> string 타입으로 추론하려면 string 타입의 키로 인덱스 시그니처를 추가한다
const obj = { [key: string]: string, //string 타입의 키로 인덱스 시그니처 추가 foo: "hello", }
- 타입 인덱싱
- 인터페이스의 속성에 타입을 지정했을 때, 해당 속성을 인덱스로 해서 타입을 가져올 수 있다
interface User { name: string age: number } const a:User['name'] = 'abc' //User[name]은 string이 된다 const b:User['age'] = 12 //User[age]은 number가 된다