개체(객체, 배열, 함수, 클래스 등)를 정의하는 타입으로 ?, readonly 키워드를 사용한다.
// 타입
typef UserT = {
name: string
age: number
}
// 인터페이스
interfcae UserI {
name: string
age: number
}
타입과 인터페이스는 외관상 약간의 차이 빼고는 거의 차이점이 없지만, 인터페이스는 객체 타입을 정의할때 보통 많이 사용된다고 한다.(타입을 써도 상관은 없다)
?
원래라면 타입이나 인터페이스를 정의할 때 적은 속성들을 모두 명시해야 하는데 ? 키워드를 통해 선택적 의미를 가지게 한다. 참고로 타입 별칭시에도 사용 가능하다.
interface UserI {
name: string
age: number
isValid? : boolean
}
const user: UserI = {
name: 'Hunoh',
age : 85,
// 있어도 되고 없어도 됨 isValid: false
}
readonly
user.age = 22
위에서 만들어진 객체에 age값을 새로할당 하는 것은 문제가 되지 않는다. 하지만 읽기전용으로 만들고 싶다면 속성앞에 readonly를 명시하면 된다. 이 또한 타입 별칭시에도 사용 가능하다.
interface UserI {
readonly name: string
age: number
isValid? : boolean
}
user.age = 22; // 에러
inertface User {
name: string
age?: number
}
// 타입 별칭
type GetUserNameT = (u: User) => string
// 인터페이스
interfcae GetUserNameI{
(u: User): string
}
const user: User = {
name: 'Hunoh'
}
const getUserName: GetUserNameI = (user: User) => user.name
const username= getUserName(user)
console.log(username)
인터페이스로 함수의 매개변수 (u: User)의 타입과 반환 타입 string을 지정할 수 있다.
호출할때 매개변수의 이름과 인터페이스 안에서 지정해주는 매개변수 이름은 달라도 된다.
interface UserI {
name: string
getName(): string
}
class User implements UserI{
public name
constuctor(name: string){
this.name=name
}
getName() {
return this.name
}
}
const user = new UserClass('Hunoh');
user.getName(); // Hunoh
function hello(userClass, msg: string){
const user = new userclass('Hunoh');
return `Hello ${user.getName()}, ${msg}`
}
hello(User, 'good morning!')
UserI는 클래스 자체의 타입이라기 보다는 클래스에서 반환되는 인스턴스의 타입을 정의하는 부분이다.
현재 생성자함수를 통해 인스턴스를 만드는데 hello 함수에서 user의 타입을 지정해 주지 않아서 에러가 나는데 자연스럽게 User타입을 지정해주면
User형식에 구문 시그니처가 없다는 에러가 나온다.
interface UserC {
new (n: string): UserI
}
function hello(userClass: UserC, msg: string){
}
위와같이 생성 시그니처를 만들고 타입으로 지정해주면 에러가 사라진다.
JS의 클래스를 이해해야한다.
인덱싱 가능한 타입을 만들때 인터페이스를 사용하는데 이 때 인덱스 시그니처 방법을 사용한다.
// Array
interface Arr {
[key: number]: string
}
const arr: Arr = ['A', 'B', 'C']
console.log(arr[1]) // 'B'
// Array-Like
interfcae ArrLike {
[key: number]: string
}
const arrLike: ArrList = {0: 'A', 1: 'B', 2: 'C'}
console.log(arrLike[1]) // 'B'
// Object
interface Obj{
[key: string]: number
}
const obj: Obj = {a: 123, b: 456, c: 789}
console.log(obj['b']) // 'B'
console.log(obj.b) // 'B'
대괄호 안의 들어갈 값의 타입 [key: number](key 문자열은 자유로 작성) , 조회한 결과 값의 타입 string 등을 각각 지정하며 동시에 배열 데이터 타입 정의가 가능하다.
interface User{
name:string
age:number
}
const user: User = {
name: 'Hunoh',
age: 99,
email: 'gnsdh8616@naver.com',
phonenumber: 123,
isValid: true // 에러
}
name 과age 속성을 제외한 나머지 속성에 대한 타입을 지정해줄 수도 있다.
지정해준 속성들만 필수로 작성해야하고 그 외의 속성은 필수가 아니다.
interface User {
[key: string]: string | number
name: string
age: number
}
const user: User = {
name: 'Heropy',
age: 85
}
console.log(user['age']) // 85
function getValues(payload: unknown){
if (payload && payload.constructor === Object){
return Object.keys(payload).map(key=>payload[key]) // 에러
}
}
getValues(user)
우선 user의 age 값에 접근할때 age에 타입이 명시되어 있기 때문에 대괄호 연산자를 통해서 접근이 가능하다.
하지만, 함수 내부의 payload[key]에서는 객체 형식에서 string 형식의 매개변수가 포함된 인덱스 시그니처를 찾을 수 없다는 에러가 발생한다.
해당 함수는 객체를 인자로 받아서 키 값들을 배열로 반환하는 기능을 가지고 있는데 객체의 타입이 unknown이기 때문에 인덱스 시그니처가 없어서 key값에 접근이 불가능한 것이다.
따라서 다음과 같이 payload라는 매개변수가 인덱스 시그니처를 가지도록 인터페이스를 추가 해보자.
interfcae Payload{
[key: string]: unknown
}
function getValues(payload: Payload){}
getValues(user); // 에러
이제 해당 에러는 사라졌지만 하지만 결과 값이 어떤 타입은 unknown으로 되어있기 때문에 User 형식의 인수는 Payload 형식의 매개변수에 할당될 수 없다는 에러가 발생한다.
즉, Payload 의 경우는 인덱스 시그너처가 있지만 User의 객체 데이터 타입에는 인덱스 시그너처가 없기 때문에 일치 하지 않는다는 뜻이다.
따라서, User 객체에도 인덱스 시그너처를 추가해야 한다.
interface User {
[key: string]: string | number
name: string
age: number
}
interface User {
name: string
age: number
}
const a: User['name'] = 123 // 에러
cosnt b: User['age'] = 123
인터페이스의 속성의 또 다른 타입을 다른 변수의 타입에 할당할 수 있다.
interface User {
name: string
age: number
}
interface UserB extends UserA {
isValid: boolean
}
인터페이스도 클래스의 extends와 같이 확장 가능하다.
interface User {
name: string
age: number
}
interface User {
isValid: boolean
name: age // 에러
}
같은 이름의 인터페이스를 중복 선언이 가능하고, 이전의 선언을 덮어쓰는 것이 아니라 합쳐진다.
하지만, 기존의 존재하는 속성을 지정할때는 다른 타입을 지정할 수는 없다.
function greet(msg: string){
return `Hello ${this.name}, ${msg}` // this 에러
}
const heropy = {
name: 'Heropy',
greet
}
const neo = {
name: 'Neo'
}
greet.call(neo, 'Have a great day!')
이 경우 자바스크립트라면 greet함수 내에서 this가 neo를 정상적으로 가리키지만 타입스크립트에서는 this가 암시적으로 any 타입으로 지정되어있어서 this가 무엇인지 명시적으로 알려줄 필요가 있다.
interface User {
name: string
}
function greet(this:User, msg: string){
return `Hello ${this.name}, ${msg}`
}
위와 같이 적어도 this가 User구조는 최소한 가지고 있어야 함을 명시한다.
동일한 하나의 함수가 여러가지의 타입 선언을 가져야 하는 상황에 사용한다.
함수 인수에 문자+문자 조합 또는 숫자+숫자 조합만 들어와야 하는 함수를 구현하려면 유니온 타입을 생각해볼 수 있다.
function add(x: string | number, y: string | number): string | number {
return x+y
}
하지만 이 경우, 인자로 숫자+문자, 문자+숫자가 들어올 경우 에러가 발생한다.
둘 타입의 일치를 강제할 수 없기 때문이다.
function add(x: string, y: string): string
function add(x: number, y: number): number
function add(x: any, y: any): any {
console.log(x, y)
return x+y
}
add('Hello', 'World')
add(12, 34)
add(12, 'Hello')
하지만 오버로딩을 통해서 이 문제를 해결할 수 있다.
함수의 구현된 내용은 표현하지 않고 타입 선언하는 부분인 선언부와 구현부를 나눠 오버로딩을 구현할 수 있다.
따라서 호출 시 string+string 조합과 number+number조합만 가능하게 만들어 타입을 더 엄격히 검사한다.
자바와 같이 타입스크립트에서도 접근 제어자를 사용할 수 있다.
사용법은 변수나 함수 앞에 접근 제어자를 명시하면 된다.
class Animal {
constructor(
private name: string,
public readonly sound: string
){}
}
위와 같이 축약해서 표현이 가능하다.(this.name=name, this.sound = sound 생략해도 사용 가능)
클래스의 특정 구조를 강제하는 용도를 사용하며 인터페이스와 비슷하여 차이점을 중심으로 알아보자.
// 추상 클래스
abstract class AnimalA {
abstract sound: string
abstract color: string
abstract getColor(): string
abstract speak(): string
}
class Dog extends AnimalA {
constructor(
public sound: string,
public color: string
) {
suer()
}
getColor() {
return this.color
}
}
cosnt dog = new Dog('멍멍!', 'white')
dog.speak() // '멍멍!'
dog.getColor() // 'white'
// 인터페이스
interface AnimalI {
sound: string
color: string
speak(): string
getColor(): string
}
class Cat implements AnimalI {
constructor(
public sound: string,
public color: string
) {
// super()
}
speak() {
return this.sound
}
getColor() {
return this.color
}
const cat = new Cat('야옹~', 'yellow')
cat.speak()
cat.getColor()
super()
extends를 통해서 클래스를 상속하므로 생성자함수에서 super()를 필수적으로 호출해야 함super() 호출 하지 않아도 됨abstract
abstract를 붙여야 함추상클래스와 달리 다중사용이 가능하다.(Has-A 관계)
class Cat implements AnimalT1, AnimalT2{}
이렇게만 보면 추상 클래스가 인터페이스가 훨씬 편해서 추상 클래스를 왜 쓸까 하는 의문점이 생긴다. 당연하게도 추상 클래스만의 기능이 있으므로 알아보자.
abstract class AnimaiA{
abstract color: string
abstract getColor(): string
constructor(public sound: string) {}
speak() {return this. sound}
}
class Dog extends AnimalA{
constructor(
sound: string,
public color: string
) {
super(sound)
}
getColor() { return this.color}
}
참고로 클래스와 인터페이스를 동시에 연결도 가능하다.
class Cat extends CatA implements AnimalT1, AnimalI2 {}
그래서 뭘 써야 할까..?
클래스에 추가 기능을 붙이거나 기능을 변경하는 기능으로 중요도는 높지 않다.
사용법은 @deco 키워드를 사용하는데 클래스, 변수, 메서드 앞에 적용이 가능하다.
function deco(target: any){
return class {
constructor(public a: any){
console.log(this.a) // hunoh Choi
}
} as any
}
@deco
class User {
constructor(public name: string) {}
hello(msg: string) {
return `Hello ${this.name}, ${msg}`
}
}
const heropy = new User('hunoh)
const neo = new User('Choi')
target에는 User 클래스가 인수로 들어가고 데코레이터가 반환하는 클래스가 앞으로 사용하는 User클래스가 된다.
결국 데코레이터를 통해 우리가 만들어놓은 User클래스와 전혀 다른 클래스를 반환하여 사용할 수 있는 것이다.
생성 시그니처, 호출 시그니처에 대해서 개념을 잘 알고 있다면 오류 수정하는데 시간을 많이 세이브 할 수 있을 것 같다. 추상 클래슬아 인터페이스는 아직 이해를 제대로 못해서 나중에 다시 봐야겠다.