Type alias VS Interface

장달진·2025년 2월 28일

새로운 현상 발견


Prisma에서 JSON 컬럼을 사용하게 되면 JsonValue라는 타입으로 떨어지는데 이것을 타입추론을 해서 사용한다.

//Prisma.JsonValue
export declare type JsonValue =
  | string
  | number
  | boolean
  | null
  | JsonObject
  | JsonArray

위와 같이 JsonValue는 이렇다

interface Foo {
 	a : number 
}

type Bar {
	a : number
}

//가능
const foo1: Prisma.JsonValue = something as unknown as Foo;
//가능
const bar2 : Prisma.JsonValue = somthing as unknown as Bar
//불가능
const foo1: Prisma.JsonValue = something as Foo;
//가능
const bar2 : Prisma.JsonValue = somthing as Bar

이렇게 두개의 사용이 달랐는데 사내 사람들중 아무도 명확하게 왜 그런지 몰라서 내가 직접 해봐야겠다고 생각했다. 🤔

단순 사용 빌드 전후


TS

interface Foo {
  a: string;
  b: number;
  c?: Foo;
}

type Bar = {
  a: string;
  b: number;
  c?: Bar;
};

function main() {
  const foo: Foo = {
    a: 'foo1',
    b: 1,
    c: {
      a: 'foo2',
      b: 2,
    },
  };

  const bar: Bar = {
    a: 'bar2',
    b: 3,
    c: {
      a: 'bar2',
      b: 4,
    },
  };

  console.log(foo);
  console.log(bar);
}
main();

JS

"use strict";
function main() {
    const foo = {
        a: 'foo1',
        b: 1,
        c: {
            a: 'foo2',
            b: 2,
        },
    };
    const bar = {
        a: 'bar2',
        b: 3,
        c: {
            a: 'bar2',
            b: 4,
        },
    };
    console.log(foo);
    console.log(bar);
}
main();

js결과물 에서는 interface, type이 없다. (tsconfig에서 declation을 true로 두면 d.ts로 빠진다)

상속 빌드 전후


TS

interface Foo {
  a: string;
  b: number;
  c?: Foo;
}

type Bar = {
  a: string;
  b: number;
  c?: Bar;
};

class ExtenedFoo implements Foo {
  a: string;
  b: number;
  c: Foo;
  constructor() {
    this.a = 'foo1';
    this.b = 1;
    this.c = {
      a: 'foo2',
      b: 2,
    };
  }
}

class ExtendedBar implements Bar {
  a: string;
  b: number;
  c: Bar;
  constructor() {
    this.a = 'bar1';
    this.b = 3;
    this.c = {
      a: 'bar2',
      b: 4,
    };
  }
}

function main() {
  const foo: Foo = new ExtenedFoo();
  const bar: Bar = new ExtendedBar();

  console.log(foo);
  console.log(bar);
}
main();

JS

"use strict";
class ExtenedFoo {
    a;
    b;
    c;
    constructor() {
        this.a = 'foo1';
        this.b = 1;
        this.c = {
            a: 'foo2',
            b: 2,
        };
    }
}
class ExtendedBar {
    a;
    b;
    c;
    constructor() {
        this.a = 'bar1';
        this.b = 3;
        this.c = {
            a: 'bar2',
            b: 4,
        };
    }
}
function main() {
    const foo = new ExtenedFoo();
    const bar = new ExtendedBar();
    console.log(foo);
    console.log(bar);
}
main();

interface , type도 다 없어지고 optional과 property type도 다 사라졌다. interface는 이름만 같으면 병합되고 type은 유니온 타입을 할 수 있다는 사용법 말고는 동일하다는 것인가 🧐

Prisma JSON 따라만들기


interface Foo {
  a: string;
  b: number;
  c?: boolean;
}

type Bar = {
  a: string;
  b: number;
  c?: boolean;
};

type JsonValue<T = unknown> =
  | string
  | number
  | boolean
  | null
  | Record<string, T>
  | Array<T>;

function main(arg: JsonValue) {
  if (arg !== null && Array.isArray(arg) && 'a' in arg && 'b' in arg) {
    //가능
    const foo = arg as Foo;
  }
  //에러
  const foo = arg as Foo;
  //가능
  const foo2 = arg as unknown as Foo;
  //가능
  const foo3 = arg as Foo | null;
  //가능
  const bar = arg as Bar;
}
main(null);

foo만 에러나야하는데 foo3은 에러가 안 나서 더 미스테리 해졌다 😇

GeminiChatGPT 4o는 단순히 interface가 as 문법시 더 강력하게 확인한다라고만 하는데 그렇다면 foo3은 또 왜 되는것인지 알아내지 못했다

위에서 JsonValue에서 null을 빼고 테스트 해도 foo는 실패한다. 어딜 뒤져봐도 안 나와서 글을 쓰는 김에 새로 type과 interface를 동일하게 사용하는 방법을 작성해봐야겠다.

Tuple


interface ITuple {
  0: string;
  1: number;
}

type TTuple = [string, number];

const a: ITuple = ['a', 1];
const b: TTuple = ['b', 2];

const c = b;
const d = a;

//에러
const [n1, s1] = a;
//가능
const [n2, s2] = b;

c 와 d를 만들때까지만 해도 상호호환 되는 것 같지만 구조분해를 하면 안된다고 한다.
'ITuple' 형식에는 반복기를 반환하는 '[Symbol.iterator]()' 메서드가 있어야 합니다.

interface ITuple {
  0: string;
  1: number;

  [Symbol.iterator](): IterableIterator<string | number>;
}

type TTuple = [string, number];

const a: ITuple = ['a', 1];
const b: TTuple = ['b', 2];

const c = b;
const d = a;

//에러
const [n1, s1] = a;
for (const v of a) {
  console.log(v);
}
//가능
const [n2, s2] = b;
for (const v of b) {
  console.log(v);
}

ITuple은 object취급되어 Tuple로 사용하기 위해 반복기(iterator)가 필요하고 TTuple은 Array 취급되어 가능한 것이다.

Function


interface IFunction {
  (a: number, b: number): string;
}

type TFunction = (a: number, b: number) => string;

const fa: IFunction = (a, b) => `${a} + ${b}`;
const fb: TFunction = (a, b) => `${a} + ${b}`;

const fc = fa;
const fd = fb;

console.log(fa(1, 2));
console.log(fb(3, 4));
console.log(fc(5, 6));
console.log(fd(7, 8));

함수는 크게 특별한게 없이 형식만 맞춰주면 호한이 되는 것을 알 수 있다.

결론


Type Alias와 Interface중 한개만 사용하라고 하면 Type Alias를 선택하겠지만
ESlint의 Stylistic을 적용해서 예제를 돌려보면
@typescript-eslint/prefer-function-type Interface only has a call signature, you should use a function type instead
@typescript-eslint/consistent-type-definitions Use an interface instead of a type

inteface와 type alias가 굉장히 비슷한 역할을 하지만 interface는 조금 엄격한 검사가 있기에 object를 위한건 interface 그 외에는 type으로 작성하는 것을 추천(?) 하고 있다. 줏대가 없다면 여기에 맞추는 것도 나쁘지 않겠다

ESlint

깃허브


달진이네 깃허브

profile
아무것도 모르는 개발자

0개의 댓글