interface vs. type
type 키워드로 타입 별칭(Type alias)을 선언하는 것처럼, interface 로도 타입을 선언할 수 있다. 둘은 문법이 다를 뿐, 거의 같은 기능을 수행한다. 하지만 세세한 부분에 차이점이 존재한다.
1. interface 키워드 옆에는 형태가 온다
타입 별칭(type 키워드)은 더 일반적이어서,
타입별칭 오른쪽에는 타입 표현식(타입, &, | 등의 타입 연산자)을 포함한 모든 타입이 올 수 있다.
반면, interface 키워드 옆에는 반드시 형태가 와야한다!
type은 복잡한 타입 표현식에 이름을 붙여주는 것과 같고,
interface 는 객체의 형태를 정의하기 위한 방법으로 설계되었다.
type SomethingAlias = number | string
interface SomethingShape {
nickName: string;
level: number;
} //형태가 온다.여기서, 타입 표현식이란?
typescript 공식문서에 나오는 개념은 아니지만, 식(Expression))과 문(Statement) 개념으로 이해할 수 있다.
먼저 프로그래밍에서 "식"과 "문"에 대한 개념을 정리하면,
- 식(Expression): 식은 값을 만들어낸다. 예를 들어,
2 + 2는 4라는 값을 만들어내므로 식이다. 또는x도 변수x의 값을 만들어내므로 식.let y = 2 + 2;에서2 + 2;부분도 식. - 문(Statement): 문은 일련의 작업을 수행하지만, 자체적으로는 값을 만들어내지 않는다. 문은 일반적으로 프로그램의 최상위 레벨 또는 블록
{ ... }내에서 위치한다. 예를 들어,let x = 2 + 2;는x라는 이름의 변수를 선언하고,2 + 2라는 식의 결과값인4를x에 할당하는 작업을 수행하는 문.
타입 표현식(type expression)과 타입 문(type statement) 개념도 비슷함.
- 타입 표현식(Type Expression): 타입 표현식은 타입을 나타내는 식. 즉, 기본 타입, 배열 타입, 유니온 타입 등과 타입연산자는 모두 타입 표현식의 일부가 될 수 있다.
예를 들어,
string이나number | string등은 타입 표현식Array<string>처럼 다른 타입 표현식을 포함하는 것도 가능. - 타입 문(Type Statement): 타입 문은 타입을 정의하거나 이름을 붙이는 등의 작업을 수행하지만, 자체적으로는 타입을 나타내지 않는다. 예를 들어,
type MyString = string;는MyString이라는 새 타입을 선언하고,string타입을 **MyString에 연결하는 타입 문.
물론, 같은 형태를 정의한다면 type으로 선언한 자리에 interface로 선언한 타입이 올수 있다.
type Sushi = {
calories: number;
salty: boolean;
price: number;
} interface Sushi {
calories: number
salty: boolean
price: number
}
const unagiSushi: Sushi = {
calories: 11
salty: 15;
price: 20
}2. 타입을 조합하는 방법
타입을 조합할 때는 조금 다른 방식이 적용된다.
type Cake = {
calories: number
sweet: boolean
price: boolean
}Cake , Sushi 는 공통의 프로퍼티를 포함하기 때문에 Food라는 상위 카테고리에 해당하는 타입을 선언하고 이를 이용해서 다시 정의할 수 있다.
type alias with intersection(& )
type Food = {
calories: number
price: number
}
type Sushi = Food & {
salty: boolean
}
type Cake = Food & {
sweet: boolean
}type alias로 선언한 경우, & 연산자를 통해 타입을 합친다.
Interface with extends
interface의 경의 extends 키워드로 타입 선언 시 다른 인터페이스를 상속받을 수 있다.
이 때 항상 Interface로 선언한 것만 가능한 것이 아니라, 객체타입, 클래스도 가능.
interface Food {
calories: number
price: number
}
interface Sushi extends Food {
salty: boolean
}
interface Cake extends Food {
sweet: boolean음.. 그럼 사용하는 문법만 다르고 하는 역할은 똑같은거 아니야? 라고 생각할 수 있다.
위의 예시에서는 결과적으로 Sushi, Cake 프로퍼티의 차이는 없지만, 둘을 사용하는 조건마다 미묘한 차이가 있다. 계속 차이점을 알아보자.
3. 상속할 때 확인하는 것
타입스크립트는 인터페이스를 상속할 때, 상속받는 인터페이스의 타입에 상위 인터페이스를 할당할 수 있는지 확인한다. 예를 들어,
interface A {
getString(x: number): string
getNumber(x: number): number
}
interface B extends A {
getString(x: string | number): string // OK (number가 할당될 수 있음)
getNumber(x: string): string // error
}Interface 'B' incorrectly extends interface 'A'.
Types of property 'getNumber' are incompatible.
Type **'(x: string) => string' is not assignable to type '(x: number) => number'.**
Types of parameters 'x' and 'x' are incompatible.
Type **'number' is not assignable to type 'string'
=> A 인터페이스의 nubmer 타입 파라미터가 B 인터페이스의 string 타입 파라미터에 할당될 수 없음을 경고.**하지만, intersection 타입을 사용하는 경우 타입스크립트는 최대한 타입들을 조합하는 방식으로 결과를 낸다.
위와 같은 예시에서 type 을 사용하면, 컴파일 에러가 발생하지 않고, 오버로드 시그니처가 만들어진다.
type A = {
getString(x: number): string
getNumber(x: number): number
}
type B = A & {
getString(x: string | number): string
getNumber(x: string): string
}TS Playground - An online editor for exploring TypeScript and JavaScript (opens in a new tab)
4. 같은 이름의 타입 선언 가능 여부 - 선언 병합
같은 범위에 같은 이름의 Type alias 선언은 불가능하지만, interface는 가능하다. 이때 여러개의 같은 이름을 가진 인터페이스들은 자동으로 합쳐진다. 이를 *선언 병합(declaration merging)*이라고 한다.
선언 병합은 열거형 타입(Enum)에서도 사용되는 타입스크립트 기능이다.
interface User {
name: string
}
interface User {
age: number
}
interface User {
email: string
}
const elice: User = {
name: 'elice',
age: 8,
email: 'elice@elicer.com'
}같은 코드를 type 키워드를 사용하여 표현하면 중복된 Duplicate identifier 'User' 와 같이 에러가 발생한다.
하지만 이렇게 유연성 있어보이는 interface도 사용할 때 주의할 점이 있다.
병합 시 같은 프로퍼티 간의 충돌
interface User {
name: string;
}
interface User {
name: string | boolean;
// Error: Subsequent property declarations must have the same type.
// Property 'name' must be of type 'string', but here has type 'boolean'.(2717)
// input.tsx(32, 2): 'name' was also declared here.
}위에서 같은 프로퍼티인 name 에 새로운 타입을 할당하면서 충돌이 발생했다. 같은 프로퍼티를 다른 곳에서 선언하여 병합 시, 반드시 같은 프로퍼티는 같은 타입을 가져야한다.
따라서, name: boolean | string 과 같이 선언해줘도 에러는 발생한다.
TS Playground - An online editor for exploring TypeScript and JavaScript (opens in a new tab)
제네릭으로 interface를 선언했다?
일단 잘 병합되지 않는다. 하지만 제네릭의 선언 방법, 같은 타입의 제네릭 매개변수를 가지면 병합된다.
interface User<Name extends string>{
name: Name;
}
interface User<Name extends boolean>{
name: Name;
}
// All declarations of 'User' must have identical type parameters예를 들어, 아래와 같이 **MyInterface<T>를 두 번 선언하고 동일한 제네릭 파라미터 **T를 사용하는 경우 병합이 발생한다.
TS Playground - An online editor for exploring TypeScript and JavaScript (opens in a new tab)
interface MyInterface<T> {
prop1: T;
}
interface MyInterface<T> {
prop2: T;
}
const obj: MyInterface<string> = {
prop1: "Hello",
prop2: "World",
};위의 예시에서는 **MyInterface<T>를 두 번 선언했지만, **T가 동일하므로 타입스크립트가 자동으로 이 두 인터페이스를 병합하여 하나의 인터페이스로 취급한다.
결과적으로 **obj 객체는 **prop1과 **prop2를 모두 포함하는 타입을 갖게 됨.
다만, 제네릭 파라미터를 사용하는 복잡한 경우에는 병합이 제대로 이루어지지 않을 수 있으며, 이러한 경우에는 별도의 방법을 사용해야 할 수도 있습니다. 제네릭을 사용하는 복잡한 인터페이스 병합이 필요한 경우에는 인터페이스를 확장한 타입 별칭을 사용하거나, 유니언 타입 등을 활용하여 원하는 병합 결과를 얻을 수 있습니다.
5. 클래스와 사용하기
클래스를 선언할 때 implements 키워드로 특정 인터페이스를 만족시킨다는 것을 표현할 수 있다.
다른 명시적 타입 어노테이션처럼, implements 으로 타입 수준의 제한을 추가하는 것.
특히 디자인 패턴 구현시 많이 사용된다.
interface Developer {
name: string;
language: string[];
sleep(hours: number): void;
work(hours: number): void;
}
class EliceDeveloper implements Developer {
name: 'elicer'
language: ['TypeScript', 'JavaScript', 'Python']
sleep(hours: number) {
console.log(`Slept for ${hours} hours`);
if (hours > 10) console.log('you are late!! wake up!')
}
work(hours: number) {
console.log(`worked for ${hours} hours`);
if (hours > 13) console.log('you can take a taxi onn your way home from work.')
}
}여기서 Developer 를 구현하는 ElicerDeveloper 는 Developer 의 모든 메서드를 구현해야 하며, 추가로 필요한 메서드, 프로퍼티를 구현할 수 있다.
또한, 하나의 클래스가 여러 인터페이스를 구현할 수도 있다.
enum Project {
landing,
LXP,
test,
works,
designSystem,
admin,
account
}
interface PlatformTeam {
squad: string;
project: Project;
}
class EliceDeveloper implements Developer, PlatformTeam {
name = 'elicer'
language= ['TypeScript', 'JavaScript', 'Python']
sleep(hours: number) {
console.log(`Slept for ${hours} hours`);
if (hours > 10) console.log('you are late!! wake up!')
}
work(hours: number, /*hasHotfix: boolean */) {
console.log(`worked for ${hours} hours`);
if (hours > 13) console.log('you can take a taxi onn your way home from work.')
}
// PlatformTeam 구현 프로퍼티
squad = 'Web B'
project = Project.designSystem
// 추가한 메서드와 프로퍼티.
deploy(issueCount: number ) {
issueCount < 3 ? console.log('칼퇴') : console.log('야근')
}
}이렇게 implements 키워드를 이용하면 타입 안정성을 보장받을 수 있다.
⇒ 프로퍼티를 빼먹거나 리턴하는 타입이 다른 메소드 등에 타입스크립트가 지적해준다.
type alias와 interface 차이점 정리.
interface
- 주로 객체의 형태(shape)를 설명하는 데 사용된다.
- 선언 병합(declaration merging)이 가능.
- 확장(extends) 및 구현(implements)이 가능.
**interface의 우측에 오는 것은 구체적인 객체의 형태를 나타내는 속성과 메서드의 목록이며, 타입 표현식이 X
interface Person {
name: string;
age: number;
}type alias
**type 별칭은 새로운 타입의 이름을 선언하는 데 사용되며, 기존 타입에 대한 참조 또는 더 복잡한 타입 표현식을 단순화하는 데 사용될 수 있음. **type 별칭은 타입 표현식에 이름을 붙이는 것이므로, 별칭의 우측에는 타입 표현식이 오게됨.
type StringOrNumber = string | number;이처럼 **interface와 **type은 각각의 문맥에서 특정 형태의 타입을 나타내는 방법으로 사용됩니다. **interface는 객체의 형태를 나타내는 구조를 제공하고, **type은 기존 타입에 이름을 붙이거나 복잡한 타입을 단순화하는데 사용됩니다. 이런 차이 때문에 **interface와 **type 우측에 올 수 있는 것이 다릅니다.
inteface 구현(implements) vs. 추상 클래스 상속 (extends)
인터페이스 구현은 추상 클래스 상속과 아주 비슷하다. 어떠한 차이점이 있을까?
-
인터페이스는 더 범용으로 쓰이며 가볍고,
- 가볍다: 자바스크립트 코드를 만들지 않고 컴파일 타임에만 존재한다.
-
추상 클래스는 특별한 목적과 풍부한 기능을 갖는다.
- 런타임의 자바스크립트 클래스 코드를 만든다.
- 생성자와 기본 구현을 가질수 있다.
- 프로퍼티와 메서드에 접근 한정자를 지정할 수 있다.
-
class 접근 한정자
타입스크립트에는 class에 접근 제한자(Access modifier)인
public,protected,private를 지원하며, 이를 통해 외부에서 특정 메서드나 프로퍼티에 접근 범위를 지정할 수 있다.타입스크립트만의 고유 기능이므로 컴파일 타임에만 존재하며, 응용 프로그램을 자바스크립트로 컴파일 할 때는 아무 코드도 생성하지 않는다.
물론 es2019에서 private 필드를 선언할 수 있는 방법이 추가되긴 했지만… (opens in a new tab)
public: 어디에서나 접근 가능함. (기본 한정자)protected: 이클래스, 서브 클래스의 인스턴스에서만 접근 가능.private: 이 클래스의 인스턴스에서만 접근 가능.
언제 뭘 사용해야할까?
여러 클래스에서 공유하는 구현 → 추상 클래스 사용
가볍게 이 클래스는 T다 → 인터페이스 사용
구조기반 타입을 지원하는 class
타입스크립트는 클래스를 비교할 때, 타입 이름이 아닌 구조를 확인한다.
즉, 클래스는 자신과 똑같은 프로퍼티, 메서드를 정의하는 일반 객체 및 클래스의 형태를 공유하는 다른 모든 타입과 호환된다.
class Javascript {
write() {
// ...
}
}
class TypeScript {
write()
}
// JavaScript class 타입을 매개변수로 받는 함수
const develop = (language: Javascript) => {
language.write()
}
const typescript = new TypeScript;
develop(typescript); // OK단, private이나 protected 필드를 갖는 클래스의 경우엔 달라질 수 있다.
지정된 클래스나 서브클래스의 인스턴스가 아니라면 할당할 수 없게된다.
class A {
private age = 30
}
class AB extends A {}
const run = (person: A) => {
}
run(new A) // OK
run(new AB) // OK
run({age: 30}) // error예시 - interface 확장하기
서드파티 라이브러리를 사용할 때, 해당 라이브러리에서 정의한 타입에 프로퍼티를 추가하고 싶은 경우가 있을 수 있다.
예를 들어, MUI와 같은 UI라이브러리는 이미 스타일까지 입혀진 UI 컴포넌트를 지원하지만, 사용자가 커스텀할 수 있는 다양한 방법을 지원한다.
특히, 특정 컴포넌트의 프롭을 추가하거나 특정 프롭의 선택지를 추가하는 것에서 선언 병합(declaration merging)이 아주 유용하게 사용될 수 있다
예를 들어, Chip 컴포넌트 타이핑을 살펴보면
export interface ChipPropsVariantOverrides {}
export interface ChipPropsSizeOverrides {}
export interface ChipPropsColorOverrides {}
export interface ChipTypeMap<P = {}, D extends React.ElementType = 'div'> {
props: P & {
/**
* The Avatar element to display.
*/
avatar?: React.ReactElement;
/**
* This prop isn't supported.
* Use the `component` prop if you need to change the children structure.
*/
children?: null;
/**
* Override or extend the styles applied to the component.
*/
classes?: Partial<ChipClasses>;
/**
* If `true`, the chip will appear clickable, and will raise when pressed,
* even if the onClick prop is not defined.
* If `false`, the chip will not appear clickable, even if onClick prop is defined.
* This can be used, for example,
* along with the component prop to indicate an anchor Chip is clickable.
* Note: this controls the UI and does not affect the onClick event.
*/
clickable?: boolean;
/**
* The color of the component.
* It supports both default and custom theme colors, which can be added as shown in the
* [palette customization guide](https://mui.com/material-ui/customization/palette/#adding-new-colors).
* @default 'default'
*/
**color?: OverridableStringUnion<
'default' | 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning',
ChipPropsColorOverrides
>;**
/**
* Override the default delete icon element. Shown only if `onDelete` is set.
*/
deleteIcon?: React.ReactElement;
/**
* If `true`, the component is disabled.
* @default false
*/
disabled?: boolean;
/**
* Icon element.
*/
icon?: React.ReactElement;
/**
* The content of the component.
*/
label?: React.ReactNode;
/**
* Callback fired when the delete icon is clicked.
* If set, the delete icon will be shown.
*/
onDelete?: React.EventHandler<any>;
/**
* The size of the component.
* @default 'medium'
*/
**size?: OverridableStringUnion<'small' | 'medium', ChipPropsSizeOverrides>;**
/**
* If `true`, allows the disabled chip to escape focus.
* If `false`, allows the disabled chip to receive focus.
* @default false
*/
skipFocusWhenDisabled?: boolean;
/**
* The system prop that allows defining system overrides as well as additional CSS styles.
*/
sx?: SxProps<Theme>;
/**
* @ignore
*/
tabIndex?: number;
/**
* The variant to use.
* @default 'filled'
*/
**variant?: OverridableStringUnion<'filled' | 'outlined', ChipPropsVariantOverrides>;**
};
defaultComponent: D;
}
export type ChipProps<
D extends React.ElementType = ChipTypeMap['defaultComponent'],
P = {},
> = OverrideProps<ChipTypeMap<P, D>, D>;
variant, size, color에 커스텀 프로퍼티를 사용할 수 있도록 구성되어있다.
예를 들어, EDS에서는 muted variant를 추가하여 사용하고 있다.
**declare module '@mui/material/Button' {// module augmentation**
interface **ChipPropsVariantOverrides {
muted: true;
}
interface {
}
}**mui 코드베이스에 있는 ChipPropsVariantOverrides 는 프로퍼티가 비어있는 형태이지만, 개발자가 선언 병합을 통해 프로퍼티를 추가할 수 있기 때문에 우리는 새로운 variant, color 등을 추가할 수 있는 것이다!
참고로, ChipProps는 다른 컴포넌트들과 다르게 Interface가 아닌 type 키워드로 선언되어서 prop을 추가할 수 없었다.
⇒ shape: ‘squared’ | ‘circular’ 를 추가하고 싶었으나… 불가능….
그래서 variant에 squared 를 추가했지만 다른 variant들과 성격이 다르기 때문에 별도의 컴포넌트를 추가했다.
현재 개발중인 프로젝트 내부에 있는 타입에 프로퍼티를 추가하고 싶으면 type alias 를 사용하거나 새 interface를 선언해도 가능하지만,
이처럼 라이브러리에서 사용중인 타입에 변화를 줄 때는 interface의 선언 병합이 매우 유용하다.
(덕분에 material-ui에 기본적으로 제공하는 팔레트 컬러 말고도 다양한 컬러들을 추가하여 사용할 수 있다.)