Skip to main content

타입스크립트

· 10 min read

타입 추론을 적극 활용하자

타입스크립트가 타입을 제대로 추론하면 그대로 쓰고, 틀리게 추론할때만 타입 명시 타입스트립트 추론이 일반적으로는 보다 정확함

타입스크립트에만 있는 타입

  • any
  • unknown
  • void : 반환값이 없음
  • , Object : null과 undefined를 제외한 모든 값을 의미

네임스페이스 : 네임스페이스 별로 인터페이스를 분리 가능

좁은 타입은 넓은 타입에 대입할 수 있지만, 넣은 타입은 좁은 타입에 대입할 수 없다

객체의 속성과 메서드에 적용되는 특징

객체 리터럴을 대입했냐, 변수를 대입했냐에 따라 타입 검사 방식이 달라짐

객체 리터럴 대입 시 에러

interface Example {
hello: string;
}

const ex: Example = {
hello: "hi",
why: "sdfsd", // error: Object literal may only specify known properties, and 'why' does not exist in type 'Example'.(2353)
};

const obj = {
hello: "hi",
why: "123",
};
const ex1 = obj;

함수 파라미터 대입 시 에러

interface Money {
amount: number;
unit: string;
}

const money = { amount: 100, unit: "won", error: "에러 안남" };

function addMoney(money1: Money, money2: Money) {
return {
amount: money1.amount + money2.amount,
unit: "won",
};
}
addMoney(money, { amount: 3000, unit: "money", erorr: "엘러" }); // Object literal may only specify known properties, and 'erorr' does not exist in type 'Money'.(2353)

인덱스 접근 타입 (Indexed Access Type)

type Animal = { name: string };

type N1 = Animal["name"]; // N1 타입은 string
const obj = {
hello: "world",
name: "zero",
age: 28,
};
type Keys = keyof typeof obj; // type Keys = "hello" | "name" | "age"
type Values = (typeof obj)[Keys]; // type Values = string | number

매핑된 객체 타입 (Mapped Object Type)

type HelloAndHi = {
// [key: 'hello' | 'hi'] : string //에러
[key in "hello" | "hi"]: string;
};
  1. 기존 객체 타입을 복사하는 코드 (in, keyof)
interface Original {
name: string;
age: number;
married: boolean;
}
type Copy = {
[key in keyof Original]: Original[key];
};
  1. 다른 타입으로부터 값을 가져오면서 수식어를 붙일 수 있음
interface Original {
name: string;
age: number;
married: boolean;
}
type Copy = {
readonly [key in keyof Original]?: Original[key];
};
// type Copy = {
// readonly name?: string | undefined;
// readonly age?: number | undefined;
// readonly married?: boolean | undefined;
// }
  1. 다른 타입으로부터 값을 가져오면서 수식어 제거 가능
interface Original {
readonly name?: string;
readonly age?: number;
readonly married?: boolean;
}
type Copy = {
-readonly [key in keyof Original]-?: Original[key];
};
// type Copy = {
// name: string;
// age: number;
// married: boolean;
// }
  1. 속성 이름을 바꿀 수 있음

속성 명칭 대문자로 변환

interface Original {
readonly name?: string;
readonly age?: number;
readonly married?: boolean;
}
type Copy = {
[key in keyof Original as Capitalize<key>]: Original[key];
};
// type Copy = {
// readonly Name?: string | undefined;
// readonly Age?: number | undefined;
// readonly Married?: boolean | undefined;
// }

유니언(|), 인터섹션(&)

type A = string | boolean;
type B = boolean | number;
type C = A & B; // type C = boolean

type D = {} & (string | null); // type D = string
type E = string & boolean; // type E = never
type F = unknown | {}; // type F = unknown
type G = never & {}; // type G = never

타입 상속

인터페이스를 이용한 상속

interface Animal {
name: string;
}

interface Dog extends Animal {
bark(): void;
}

interface Cat extends Animal {
meow(): void;
}

타입 별칭을 이용한 상속

type Animal = {
name: string;
};

type Dog = Animal & {
bark(): void;
};
type Cat = Animal & {
meow(): void;
};
type Name = Cat["name"];

객체 간에 대입할 수 있는지 확인

타입스크립트에서는 모든 속성이 동일하면 객체 타입의 이름이 다르더라도 동일한 타입으로 취급 Money와 Liter가 동일한 타입으로 인식

interface Money {
amount: number;
unit: string;
}

interface Liter {
amount: number;
unit: string;
}

const liter: Liter = { amount: 1, unit: "liter" };
const circle: Money = liter;

브랜드(brand) 속성을 추가 (브랜딩)해서 타입 간 구별

interface Money {
__type: "money";
amount: number;
unit: string;
}

interface Liter {
__type: "liter";
amount: number;
unit: string;
}

const liter: Liter = { amount: 1, unit: "liter" };
const circle: Money = liter;
// type 'Liter' is not assignable to type 'Money'.
// Types of property '__type' are incompatible.
// Type '"liter"' is not assignable to type '"money"'.(2322)

제네릭

인터페이스에 제네릭을 이용해서 타입 간 상속

// interface Zero {
// type: 'human',
// race: 'yellow',
// name: 'zero',
// age: 28
// }
// interface Nero {
// type: 'human',
// race: 'yellow',
// name: 'nero',
// age: 32
// }
interface Person<N, A> {
type: "human";
race: "yellow";
name: N;
age: A;
}
interface Zero extends Person<"zero", 28> {}
interface Nero extends Person<"nero", 32> {}

타입에 제네릭 이용

type Person<N, A> = {
type: "human";
race: "yellow";
name: N;
age: A;
};
type Zero = Person<"zero", 28>;
type Nero = Person<"nero", 32>;

클래스에 제네릭 이용

class Person<N, A> {
name: N;
age: A;
constructor(name: N, age: A) {
this.name = name;
this.age = age;
}
}
class Zero extends Person<"zero", 28> {}
class Nero extends Person<"nero", 28> {}

조건문과 비슷한 컨디셔널 타입

type A1 = string;
type B1 = A1 extends string ? number : boolean;

콜백 함수의 매개변수는 생략 가능하다.

function example(callback: (error: Error, result: string) => void) {}

example((e, r) => {});
example(() => {});
example((e, r) => true);

공변성과 반공변성을 알아야 함수끼리 대입할 수 있다.

  • 공변성: A->B일때 T(A) -> T(B)인 경우
  • 반공변성: A->B일때 T(B) -> T(A)인 경우
  • 이변성: A->B일때 T(A) -> T(B), T(B) -> T(A) 인 경우
  • 무공변성: A->B일때 T(A) -> T(B)도 안되고, T(B) -> T(A)도 안되는 경우

타입스크립트는 기본적으로 공변성이다.

함수의 매개변수는 반공변성을 갖고 있음(strictFunctionTypes 옵션이 체크되어야함)

Enum 타입

enum 타입을 브랜딩에 사용

enum Money {
WON,
DOLLAR,
}

interface Won {
__type: Money.WON;
}

interface Dollar {
__type: Money.DOLLAR;
}

function moneyOrDollar(param: Won | Dollar) {
if (param.__type === Money.WON) {
} else {
}
}

재귀 타입

type Recursive = {
name: string;
children: Recursive[];
};

const recur: Recursive = {
name: "tes",
children: [],
};

템플릿 리터럴 타입을 사용

type Template = `template ${string}`;
let str: Template = "template ";
let str1: Template = "template 123";

문자열 조합을 표현

type City = "seoul" | "suwon" | "busan";
type Vehicle = "car" | "bike" | "walk";
type ID = `${City}:${Vehicle}`;
const id: ID = "seoul:walk";

추가적인 타입 검사에는 satisfies 연산자

타입스크립트 4.9 버전에 satisfies 연산자가 추가됨

// Object literal may only specify known properties, but 'sriius' does not exist in type '{ sun: string | { type: string; parent: string; }; sirius: string | { type: string; parent: string; }; earth: string | { type: string; parent: string; }; }'. Did you mean to write 'sirius'?(2561)
const universe = {
sun: "star",
sriius: "star",
earth: { type: "planet", parent: "sum" },
} satisfies {
[key in "sun" | "sirius" | "earth"]:
| { type: string; parent: string }
| string;
};
universe.earth.type;

원시 자료형에도 브랜딩 기법을 사용할 수 있다.

원시 자료형 타입에 브랜드 속성을 추가하는 기법 (타입스크립트만의 기법)

type Brand<T, B> = T & { __brand: B };
type KM = Brand<number, "km">;
type Mile = Brand<number, "mile">;

function kmToMile(km: KM) {
return (km * 0.62) as Mile;
}

const km = 3 as KM;
const mile = kmToMile(km);
// kmToMile(mile)

유용한 타입을 만들자.

판단하는 타입

데코레이터

  • ClassDecoratorContext: 클래스 자체를 장식할때
  • ClassMethodDecoratorContext: 클래스 메서드를 장식할 때
  • ClassGetterDecoratorContext: 클래스 getter를 장식할 때
  • ClassSetterDecoratorContext: 클래스 setter를 장식할 때
  • ClassMemberDecoratorContext: 클래스 멤버를 장식할 때
  • ClassAccessorDecoratorContext: 클래스 accessor를 장식할 때
  • ClassFieldDecoratorContext: 클래스 필드를 장식할 때
function startAndEnd(originalMethod: any, context: any) {
function replacementMethod(this: any, ...args: any[]) {
console.log("start");
const result = originalMethod.call(this, ...args);
console.log("end");
return result;
}
return replacementMethod;
}

class A {
@startAndEnd
eat() {
console.log("eat");
}
}

const a = new A();
a.eat();