// 자바스크립트에서의
// 함수 선언문
function JsStatement(){}
// 함수 표현식
const JsExpression1 = function (){};
const JsExpression2 = () => {};
// 타입스크립트에서의
// 함수 선언문
function TsStatement(a: number): number{}
// 함수 표현식
const TsExpression1 = function (a: number): number{};
const TsExpression2 = (a: number): number => {};
타입스크립트에서는 타입 재사용이라는 관점에서 함수 선언문보다 함수 표현식이 장점을 가진다. 다만, 둘의 차이를 이해하자(함수 호이스팅)
함수 선언문은 호이스팅에 영향을 받지만, 함수 표현식은 호이스팅에 영향을 받지 않는다. 함수 선언문은 코드를 구현한 위치와 관계없이 자바스크립트의 특징인 호이스팅에 따라 브라우저가 자바스크립트를 해석할 때 맨 위로 끌어 올려진다.
함수 선언문 vs 함수 선언식 개념 및 예제
타입스크립트에서 함수 표현식을 사용하자
1. 타입 재사용: 함수의 매개변수 ~ 반환값 전체를 함수 타입으로 선언할 수 있다.
// 매개변수 interface
interface Params {
p: string;
a: boolean;
r: number;
m: symbol;
}
// return interface
interface Return {
r: string;
e: boolean;
t: number;
}
// 함수 선언문 -> 함수의 매개변수와 반환값의 타입을 따로 선언
function statement(params: Params): Return{
return {r: 'r', e: true, t:0}
}
// 함수 표현식 재사용(ExpressionFunction으로 재사용)
type ExpressionFunction = (params: Params) => Return;
// 함수 표현식
const expression1: ExpressionFunction = function (params) {
return { r: 'r', e: true, t: 0 };
};
const expression2: ExpressionFunction = (params) => ({
r: 'r',
e: true,
t: 0,
});
2. 반복되는 함수 시그니처를 하나로 통합
라이브러리는 공통 함수 시그니처를 타입으로 제공한다.
만약, 라이브러리를 직접 만든다면 공동 콜백에 타입을 제공하자
type BinaryFn = (a: number, b: number) => number;
const add: BinaryFn = (a, b) => a + b;
const sub: BinaryFn = (a, b) => a - b;
const mul: BinaryFn = (a, b) => a * b;
const div: BinaryFn = (a, b) => a / b;
3. 다른 함수의 시그니처를 참조하려면 typeof fn
을 사용하면 된다.
// 함수 전체에 타입(typeof fetch)를 적용
// -> input과 init 타입 추론 가능, 반환 타입 보장
const simpleCheckedFetch: typeof fetch = async (input, init) => {
const response = await fetch(input, init);
if (!response.ok) {
throw new Error('Request failed: ' + response.status);
}
return response;
};
🎯 요약
타입스크립트에서 함수 선언문보다 함수 표현식을 사용하자
타입스크립트에서 타입을 정의하는 방법 두 가지
1. type
2. interface
type과 interface의 공통점과 차이점
1. 공통점
- 타입의 기본 동작
// type과 interface로 타입 선언 가능
type Type = {
name: string;
};
const type_common: Type = {
name: 'lee',
};
interface Interface {
name: string;
}
const interface_common: Interface = {
name: 'kim',
};
- 인덱스 시그니처
type TypeIndexSignature = {
[key: string]: string;
};
interface InterfaceIndexSignature {
[key: string]: string;
}
- 함수 타입 정의
// 함수 타입 정의
type TypeFunction = {
(x: number): number;
};
const typeFunction: TypeFunction = (x) => 0;
// 타입 별칭(type alias)로 함수 타입 선언
type TypeAliasFunction = (x: number) => number;
const typeAliasFunction: TypeAliasFunction = (x) => 0;
interface InterfaceFunction {
(x: number): number;
}
const interfaceFunction: InterfaceFunction = (x) => 0;
- 제너릭 가능
// 제너릭 가능
type TypeGeneric<T> = {
first: T;
};
interface InterfaceGeneric<T> {
first: T;
}
- 타입 확장 가능
단, 인터페이스는 유니온 타입과 같은 복잡한 타입은 확장 못함
-> 복잡한 타입을 확장하고 싶으면type과 &
사용
// 타입 확장 가능
type Type = {
name: string;
};
interface Interface {
name: string;
}
type TypeExtended = Interface & { age: number };
interface InterfaceExtended extends Type {
age: number;
}
- 클래스 구현(implements) 가능
// 클래스 구현(implements)
type Type = {
name: string;
human: boolean;
};
interface Interface {
name: string;
}
class TypeClass implements Type {
name: string = '';
human: boolean = true;
age: number = 0;
}
class InterfaceClass implements Interface {
name: string = '';
age: number = 12;
}
❓extends와 implements 차이점
extends - 상속받고자 하는 부모 클래스를 명시
implements - 미리 추상화 된 인터페이스를 채택하여 사용
extends는 이미 구현된 메서드와 프로퍼티를 편하게 사용할 때 쓰면 유용할 거 같고, implements는 어떠한 조건을 강제할 때 사용하면 유용할 것 같다.
2. 차이점
1. 유니온 개념의 유무
type에는 유니온 타입이 있지만, interface에는 유니온 인터페이스가 없다.
type TypeAorB = 'a' | 'b'
interface InterfaceAorB{
// ... ?
}
2. type 키워드로 튜플과 배열 타입을 간결하게 표현
type은 메서드 사용 가능, interface는 불가능
3. interface만의 선언 병합 기능
선언 병합 - 컴파일러가 같은 이름으로 선언된 두 개의 개별적인 선언을 하나의 정의로 병합하는 것을 의미한다. 이 병합된 정의는 원래 두 선언 각각의 기능을 모두 가지게 된다. 병합할 선언이 몇 개든 상관 없다. 선언 병합
// 예제1
interface Box{
height: number;
width: number;
}
interface Box{
scale: number;
}
let box: Box = {height: 5, width: 6, scale: 10};
// 예제 2
// Window 인터페이스 선언 병합을 통해 속성을 추가
declare global{
interface Window{
somethingToWindow: string;
}
}
window.somethingToWindow;
// window객체에 Window의 somethingToWindow 선언 병합
1. 복잡한 타입 -> type
2. 간단한 객체 타입 -> 일관성과 보강의 관점에서 둘 다 고려
3. api에 대한 타입 선언 -> interface
🎯 요약
타입스크립트의
type
과interface
의 차이점과 공통점을 잘 이해해서 적절하게 사용하자
타입스크립트에서 타입에서도 DRY(Don't Repeat Yourself)원칙을 적용하자
타입을 재활용 하는 방법
1. 타입 선언을 통해 중복 제거
// 타입 선언을 통해 중복된 타입 제거
// 전
function distance(a: { x: number; y: number }, b: { x: number; y: number }) {}
// 후
type Point2D = {
x: number;
y: number;
}
function distance2(a: Point2D, b: Point2D){
}
2. extends로 타입을 확장하여 중복을 제거
// 타입을 확장
// 전
type Point2D = {
x: number;
y: number;
};
type Point3D = {
x: number;
y: number;
z: number;
};
// 후
type TPoint3D = Point2D & { z: number };
interface IPoint3D extends Point2D {
z: number;
}
3. 인덱싱으로 타입을 축소하고, 매핑된 타입으로 정리, Pick 사용
// 전
interface State {
userId: string;
pageTitle: string;
recentFiles: string[];
pageContents: string;
}
interface TopNavState {
userId: string;
pageTitle: string;
recentFiles: string[];
}
// 후
// 인덱싱으로 타입 축소
type TopNavState = {
userId: State['userId'];
pageTitle: State['pageTitle'];
recentFiles: State['recentFiles'];
}
// 매핑된 타입 사용
type TopNavState = {
[k in 'userId' | 'pageTitle' | 'recentFiles']: State[k];
};
// 완전하지 않은 Pick
type Pick<T,K> = {
[k in K]: T[k]
}; // 'K' 타입은 'string | number | symbol' 타입에 할당 할 수 없다.
// pg83 k는 T 타입과 무관하고 범위가 너무 넓다.
// K는 인덱스로 사용될 수 있는 'string | number | symbol'이 되어야 한다.
// K는 실제로 T의 키의 부분 집합, 즉 keyof T가 되어야 한다.
// Pick 사용
type TopNavState = Pick<State, 'userId' | 'pageTitle' | 'recentFiles'>;
// extends를 사용해 매개변수를 제한
// T는 State, K는 TopNavState
// Pick -> K는 T의 키의 부분 집합(=keyof T)
type Pick<T,K extends keyof T> = { [k in K]: T[k]};
/*1. keyof는 T 타입을 받아서 속성 타입의 유니온을 반환 -> ('userId' | 'pageTitle' | 'recentFiles' | 'pageContents')
2. 아이템 7에서, 타입을 extends 하는 것은 해당 타입의 부분 집합을 의미
3. 이 부분 집합을 K로 넘겨줌
4. 매핑된 타입(in keyof Type)을 이용하여 T 타입의 키로 접근하여 타입의 속성에 대한 타입을 가 져옴
*/
interface SaveAction {
type: 'save'
}
interface LoadAction {
type: 'load'
}
type Action = SaveAction | LoadAction
type ActionType = 'save' | 'load' // 타입의 반복
type ActionType = Action['type'] // 타입은 'save' | 'load'
type ActionRec = Pick<Action, 'type'> // {type: 'save' | 'load'}
4. Partial을 이용하여 기존 타입들의 속성을 모두 Optional로 변경하기
type Something = {
a: number;
b: string;
c: boolean;
};
// 전
type OptionalSomething = {
a?: number;
b?: string;
c?: boolean;
};
// 후
type Partial<T> = { [k in keyof T]?: T[k] };
type OptionalSomething = { [k in keyof Something]?: Something[k] };
/*1. keyof는 타입을 받아서 속성 타입의 유니온을 반환 -> ('a' | 'b' | 'c')
2. 매핑된 타입(in keyof Type)을 이용하여 Something 내 k 값에 해당하는 속성이 있는지 찾음
3. ?로 각 속성을 선택적으로 만든다.
*/
5. typeof를 이용하여 객체의 타입을 추출하기
물론, 타입을 먼저 정의하는 것이 좋음
// 전
const INIT_OPTIONS = {
width: 640,
height: 480,
color: '#fff',
label: 'aaa',
};
interface Options {
width: number;
height: number;
color: string;
label: string;
}
후
6. ReturnType으로 함수나 메서드의 반환 값에 명명된 타입을 만들 수 있다.
function getUserInfo(userId: string){
return{
userId,
name,
age,
};
type UserInfo = ReturnType<typeof getUserInfo>;
🎯 요약
타입스크립트의 타입에서도 타입 연산과 제너릭을 사용해 DRY 원칙을 적용하자
type IndexSignatureType = {
[property: string]: string
};
// [키의 이름: 키의 타입]: 값의 타입
- 모든 키를 허용하기 때문에, 객체에 없는 키를 이용해도 타입 체크에서 에러가 나지 않는다.
- 특정 키가 필요하지 않는다.
{}
도 할당 가능- 키마다 다른 타입을 가질 수 없다.
- 타입스크립트의 언어 서비스를 제공받지 못한다.
동적 데이터(계산되고 가공되는 값)가 아니라면 되도록 인덱스 시그니처
사용을 피하고 다른 방법을 찾아야 한다.
타입의 keys들이 무엇이 될지 아는 경우는 인덱스 시그니처보다 타입을 직접 선언해 주는 것이 좋은데, 이에 편의성을 제공해주는 방법들이 있다.
1. Record 사용
Record는 키 타입에 유연성을 제공하는 제너릭 타입이다.
특히, string의 부분 집합을 사용할 수 있다.
type Record<K extends keyof any, T> = {
[P in K]: T;
};
2. 매핑된 타입 사용
type Vec3D = { [k in 'x' | 'y' | 'z']: number };
// 조건부 타입을 이용하여 특정 조건에서는 다른 타입 만들기
type Vec3D = { [k in 'x' | 'y' | 'z']: k extends 'x' ? string : number };
// 단, 인터페이스에서는 매핑된 타입 사용 불가
interface IVec3D {
[k in 'x' | 'y' |'z']: number
}
// 오류 발생: A computed property name in an interface must refer to an expression whose type is a literal type or a 'unique symbol' type.
인덱스 시그니처
사용 고려type ExternalType = {
[key: string]: string | undefined;
}
🎯 요약
동적 데이터가 아니라면 인덱스 시그니처를 사용하지 말자
자바스크립트의 배열은 Object 타입이다.
자바스크립트에서 Object는 키와 쌍으로 구성되어 있는데, 키의 타입은 string
or symbol
만 가능
자바스크립트 엔진에서 자동으로 형변환(number->string)이 되기 때문에, number 타입의 키로도 접근이 가능했던 것
타입스크립트에서는 일관성을 위해 number 타입의 키를 허용
// arr[0]은 내부적으로 arr['0']으로 바뀜
const arr = [1,2,3];
console.log(Object.keys(arr)); // 배열의 key들 ['0','1','2']
number
타입만 키로 허용하고 있지만, 런타임에서는 string
타입으로 형변환interface Array<T> {
length: number;
toString(): string;
toLocaleString(): string;
pop(): T | undefined;
push(...items: T[]): number;
// ... 생략
// number 타입만 키로 허용
[n: number]: T;
}
🎯 요약
1. 인덱스 시그니처가 필요한지 고려하고,
2. number 타입의 인덱스 시그니처가 필요하다면 타입을 만들지 말고,
Array, 튜플, ArrayLike
를 사용하자
타입스크립트의 readonly
는 변경 불가능을 위한 목적으로 사용된다.
readonly
의 두 가지 다른 쓰임새
1. 객체 타입 property 앞에 붙는 readonly
2. Array, 튜플 타입 앞에 붙는 readonly
1. 객체 타입 property 앞에 붙는 readonly
const
- 원시 타입 변수에게는 재할당 금지
- 객체에서는 const로 선언된 객체의 속성을 바꿀 수 있다.
-> 속성 변경을 막으려면 Object.freeze 사용
const object = {
property: 'good'
}
object.property = 'bad';
console.log(object.property); // bad
-> 즉, 재할당 가능과 객체의 속성 변경 가능은 서로 독립적인 내용
readonly
1. 객체의 속성 변경을 막을 수 있다.
//readonly로 객체 속성의 변경 막기
type ReadonlyType = {
readonly prop: string;
}
const test: ReadonlyType = {
prop: 'a'
}
test.prop ='b';
// Cannot assign to 'prop' because it is a read-only property
2. readonly가 얕게(shallow) 동작한다.
//readonly shallow
type InnerType = {
innerProp: string;
}
type ReadonlyType = {
readonly prop: InnerType;
}
const test: ReadonlyType = {
prop: {
innerProp: 'inner'
}
}
test.prop.innerProp = 'change'; // 통과
test.prop = {
innerProp: 'error'
}
// Cannot assign to 'prop' because it is a read-only property.
- 유틸리티 타입
Readonly
는 기존 타입의 속성들 모두readonly
로 변경한다.
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
2. Array, 튜플 타입 앞에 붙는 readonly
readonly
가 있으면, 배열의 요소를 읽을 수 있으나 새롭게 추가하거나 변경할 수 없다.
- 배열을 변경하는
pop, push
와 같은 함수들을 사용할 수 없다.
length
속성을 읽을 수 있으나 변경은 불가능하다.
❓ pg95 -> number[] 타입의 기능이 더 많으니까 상위 집합 아닌가?
readonly number[]
타입보다는 number[]
타입의 기능이 더 많다.readonly number[]
타입은 number[]
타입의 상위 집합이다.number[]
(변경 가능한 배열) 타입은 readonly number[]
(readonly 배열) 타입에 할당 가능🎯 요약
readonly를 사용하여 변경하면서 발생하는 오류를 방지하자
interface ScatterProps{
// The data
xs: number[];
ys: number[];
// display
xRange: [number, number];
yRange: [number, number];
color: string;
// events
onClick:(x: number, y: number, index: number) => void;
}
const REQUIRES_UPDATE:{[k in keyof ScatterProps]: boolean} = {
xs: true,
ys: true,
xRange: true,
yRange: true,
color: true,
onClick: false,
}
// good1
// 보수적 접근법(실패에 닫힌 접근법)
// 새로운 속성이 추가되면 shouldUpdate 함수는 값이 변경될 때마다 차트를 다시 그림
function shouldUpdate(oldProps: ScatterProps, newProps: ScatterProps){
let k: keyof Scatterprops;
for(k in oldProps){
if(oldProps[k] !== newProps[k]){
if(k !== 'onClick')
return true;
}
}
return false;
}
// good2
// 실패에 열린 접근법
// 차트의 불필요하게 그려지는 단점은 해결되지만, 누락될 수 있다.
function shouldUpdate(oldProps: ScatterProps, newProps: ScatterProps){
return(
oldProps.xs !== newProps.xs ||
oldProps.ys !== newProps.ys ||
oldProps.xRange !== newProps.xRange ||
oldProps.yRange !== newProps.yRange ||
oldProps.color !== newProps.color ||
);
}
// Best
// 매핑된 타입과 객체를 사용하여 타입체커가 동작하도록 한다.
// [k in keyof ScatterProps]는 타입 체커에게 REQUIRES_UPDATE가
// ScatterProps와 동일한 속성을 가져야 한다는 정보를 제공한다.
function shouldUpdate(oldProps: ScatterProps, newProps: ScatterProps){
let k: keyof ScatterProps;
for(k in oldProps){
if(oldProps[k] !== newProps[k] && REQUIRES_UPDATE[k]){
return true;
}
}
return false;
}
🎯 요약
매핑된 타입을 사용하여 값을 동기화하자
☑️ 참고 자료
함수 선언식, 함수 표현식
extends와 implements
동적 데이터
Array 인터페이스
선언 병합