12 점진적인 타입스크립트 적용 방식 3단계 - Vuex 스토어 타입 정의

source: categories/study/vue-beginner-lv5/vue-beginner-lv5_9-03.md

12.1 스토어 TS 파일 변환 및 컴포넌트의 타입 추론 문제 소개

  • store/index.js 파일을 store/index.ts 파일로 수정
  • 수정해도 딱히 타입스크립트 에러는 발생하지 않는다.
  • store/index.ts를 컴포넌트에서 가져다 쓸 때 어떤 문제가 생기는지 보자.

12.1.1 store/index.js 파일을 store/index.ts 파일로 수정하고나서 컴포넌트에서 가져다 쓸 때 발생하는 에러

  • this.$store.state. : 여기서 state에 무엇이있는지 추론이되질 않는다.
  • 타입스크립트를 사용하는 의미가 없게된다.
Note

12.1.2 스토어 TS 파일 변환 및 컴포넌트의 타입 추론 문제 소개

12.2 스토어의 타입 추론이 안되는 이유 분석



// node_modules/vuex/types/vue.d.ts
/**
 * Extends interfaces in Vue.js
 */

import Vue, { ComponentOptions } from "vue";
import { Store } from "./index";

declare module "vue/types/options" {
  interface ComponentOptions<V extends Vue> {
    store?: Store<any>;
  }
}

declare module "vue/types/vue" {
  interface Vue {
    $store: Store<any>;
  }
}


  • 위에 보시면 $store의 타입을 Vue 내부에 Store&lt;any&gt;로 정의해놨다.
  • 그런데 정의된 코드를 보시면 node_modules/vue/types 폴더 안에 vue.d.ts에 정의되어있는 interface Vue$store 타입을 확장해서 넣어놓았다.
  • 아래와 같이 node_modules/vue/types/vue.d.ts 파일에 이미 여러 타입들이 정의되어있고 여기에 vuex/types/vue.d.ts에서 $store에 대한 타입을 확장했다는 뜻이다.


// node_modules/vue/types/vue.d.ts
// ...

export interface Vue {
  readonly $el: Element;
  readonly $options: ComponentOptions<Vue>;
  readonly $parent: Vue;
  readonly $root: Vue;
  readonly $children: Vue[];
  readonly $refs: { [key: string]: Vue | Element | (Vue | Element)[] | undefined };
  readonly $slots: { [key: string]: VNode[] | undefined };
  readonly $scopedSlots: { [key: string]: NormalizedScopedSlot | undefined };
  readonly $isServer: boolean;
  readonly $data: Record<string, any>;
  readonly $props: Record<string, any>;
  readonly $ssrContext: any;
  readonly $vnode: VNode;
  readonly $attrs: Record<string, string>;
  readonly $listeners: Record<string, Function | Function[]>;

  $mount(elementOrSelector?: Element | string, hydrating?: boolean): this;
  $forceUpdate(): void;
  $destroy(): void;
  $set: typeof Vue.set;
  $delete: typeof Vue.delete;
  $watch(
    expOrFn: string,
    callback: (this: this, n: any, o: any) => void,
    options?: WatchOptions
  ): (() => void);
  $watch<T>(
    expOrFn: (this: this) => T,
    callback: (this: this, n: T, o: T) => void,
    options?: WatchOptions
  ): (() => void);
  $on(event: string | string[], callback: Function): this;
  $once(event: string | string[], callback: Function): this;
  $off(event?: string | string[], callback?: Function): this;
  $emit(event: string, ...args: any[]): this;
  $nextTick(callback: (this: this) => void): void;
  $nextTick(): Promise<void>;
  $createElement: CreateElement;
}

// ...


Note

vue3가 아니더라도 vue2에서도 간단하게 타입추론이 일어날 수 있도록 타입 정리를 해놓은 것을 볼 수 있다.

  • 여튼 vuex/types/vue.d.ts, vue/types/vue.d.ts에서 동일하게 interface Vue를 가져다 썼으므로 인터페이스 머징이라는 것이 일어나게된다.
  • 인터페이스 머징
  • declaration merging: 선언을 합친다.


// node_modules/vuex/types/vue.d.ts
/**
 * Extends interfaces in Vue.js
 */

import Vue, { ComponentOptions } from "vue";
import { Store } from "./index";

declare module "vue/types/options" {
  interface ComponentOptions<V extends Vue> {
    store?: Store<any>;
  }
}

declare module "vue/types/vue" {
  interface Vue {
    $store: Store<any>;
  }
}


  • 여튼 vuex를 썼을 때 interface Vue가 확장된다.
  • 여기서 제일 중요한 것은 Store&lt;&gt; 여기에 any라는 타입이 정의되어있기 때문에
    실제로 컴포넌트 레벨에서 $store에 접근하게되면
    즉, this.$store에 마우스 커서를 올리면 any라고 타입이 추론되게 된다.
  • 즉, this.$store 안에 내부적으로 설계한 타입들이 전부 다 any를 기준으로 추론되게 된다.

this.$store 안에 내부적으로 설계한 타입들이 전부 다 any를 기준으로 추론되게 된다 라는 말은 무슨말?



// node_modules/vuex/types/vue.d.ts
/**
 * Extends interfaces in Vue.js
 */

import Vue, { ComponentOptions } from "vue";
import { Store } from "./index";

declare module "vue/types/options" {
  interface ComponentOptions<V extends Vue> {
    store?: Store<any>;
  }
}

declare module "vue/types/vue" {
  interface Vue {
    $store: Store<any>;
  }
}


위 코드의 Store에 마우스 커서 올려놓고 ctrl + b.



// node_modules/vuex/types/index.d.ts
// ...

export declare class Store<S> {
  constructor(options: StoreOptions<S>);

  readonly state: S;
  readonly getters: any;

  replaceState(state: S): void;

  dispatch: Dispatch;
  commit: Commit;

  subscribe<P extends MutationPayload>(fn: (mutation: P, state: S) => any, options?: SubscribeOptions): () => void;
  subscribeAction<P extends ActionPayload>(fn: SubscribeActionOptions<P, S>, options?: SubscribeOptions): () => void;
  watch<T>(getter: (state: S, getters: any) => T, cb: (value: T, oldValue: T) => void, options?: WatchOptions): () => void;

  registerModule<T>(path: string, module: Module<T, S>, options?: ModuleOptions): void;
  registerModule<T>(path: string[], module: Module<T, S>, options?: ModuleOptions): void;

  unregisterModule(path: string): void;
  unregisterModule(path: string[]): void;

  hasModule(path: string): boolean;
  hasModule(path: string[]): boolean;

  hotUpdate(options: {
    actions?: ActionTree<S, S>;
    mutations?: MutationTree<S>;
    getters?: GetterTree<S, S>;
    modules?: ModuleTree<S>;
  }): void;
}

// ...


  • 제네릭 &lt;S&gt; 여기에 any가 들어가고 그걸 통해서 다른 타입들도 추론된다.
  • 결론적으로 readonly state: S;stateany가 들어와버려서 this.$store.state에 마우스 커서를 올려놨을 때 any가 추론된다는 것이다.
  • 그래서 this.$store.state에 설정해놓은 news라던가 list라던가 jobs라던가 이런 속성들을 정의하더라도 자동으로 추론되지 않는, 이런 문제가 있는 것이다.
  • 이런 부분들을 프로젝트 레벨에서 store에 코드들을 정의했을 때 자동으로 추론되게끔 하는 방법에 대해 알아보도록 하겠다.

12.3 뷰엑스 타입 정의 방법 안내 - state

  • Cracking Vue.js Vuex 타입 정의 문서

  • Vue.extend() 방식을 이용하여 컴포넌트에 타입을 정의하고 있기 때문에
    Vue.extend() 안에서 this.$store.state 또는 this.$store.getters 등을 했을 때
    vuex에 정의한 것들이 추론될 수 있게하는 방법에 대해 정리한 글이다.

12.3.1 Vuex 기본 코드

  • 아래 기본 코드는 자바스크립트건 타입스크립트건 아래와 같이 작성해야된다.
  • 아래와 같이 기본 코드를 작성 후에 추가적으로 타입 추론을 위한 코드를 넣으면된다.


// store/index.ts
import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

const store = {
  state: {
    token: '',
  }
}

export default new Vuex.Store(store);


12.3.2 state 정의

위 기본 코드에서 state를 별도의 파일로 분리한다.
store/state.ts로 만들겠다.



// store/state.ts
export const state = {
  token: '',
}

export type RootState = typeof state;


사실 분리를 안해도되지만 분리를 하는 것을 추천드린다.
왜냐하면 타입스크립트 코드들도 같이 들어가기 때문에 store/index.ts 파일 안에 몽땅 다 작성해버리시면,
store/index.ts 파일의 내용이 엄청 많아질 것이다.

그렇게되면 코드 파악이 어려워지고 한눈에 보는 것도 어려워지 때문에 가급적이면 state, getters, mutations, actions 이런 형태로 모듈화를 해주시는 것을 추천드린다.

export type RootState = typeof state;

제일 중요한거.
export에서 type을 선언해주는 거.
즉, const state에 선언한 타입을 뽑아내고 그거를 RootState라는 타입 변수로 정의를 해준 것.

12.4 state 타입 정의

Note

12.4.1 state 타입 정의

  • 아직 추론이 제대로되지 않는다.
  • store/state.ts에 정의한 타입이 Store 타입의 제네릭으로 넘어가게끔 다음 시간에 설정해보도록 하겠다.
  • 여전히 this.$store에 마우스 커서를 올려놓으면 타입이 any로 추론되고 있다.

12.5 스토어 내부 타입에 state 타입 연결

  • storestate 타입이 컴포넌트에서 추론될 수 있도록 작업을 해보도록 하겠다.
  • Store&lt;any&gt;여기서 anyRootState로 바꿔주면 된다.

1.this.$store에 마우스커서 찍고 ctrl + b를 누른 다음에 (웹스톰 기준)

  1. anyRootState로 바꾼다.

…? 음..
근데 이건 근본적인 해결책이 아니지않나..
아직 강의 다 안들어서 그런가..
이 뒤에 뭔 다른 내용이 있으려나?



// node_modules/vuex/types/vue.d.ts
/**
 * Extends interfaces in Vue.js
 */

import { RootState } from "@/store/state";
import Vue, { ComponentOptions } from "vue";
import { Store } from "./index";

declare module "vue/types/options" {
  interface ComponentOptions<V extends Vue> {
    store?: Store<RootState>;
  }
}

declare module "vue/types/vue" {
  interface Vue {
    $store: Store<RootState>;
  }
}


이거.. 아닌거같은데.. 음..

왜냐면 해당 node_modules 폴더는 git에 올려서 클론받고 하는게 아니잖아.
package.json에 라이브러리 목록만 적어놓고 npm i로 설치하는건데,
npm i로 설치하면 어차피 위에 수정한 내용이 설치되는거 아니잖아?
즉, 그러니까 node_modules 폴더에 있는걸 수정하는건 소용없다는 뜻..
혼자 일하는거면 뭐 소용있겠지만..?

Note

강의 뒷 부분에 해당 궁금증에 대한 해결책이 있는듯?
좀 더 봐보자.

12.6 mutations 타입 정의 안내

12.6.1 mutations 정의

뮤테이션 코드도 store/mutation.ts 파일에 별도로 작성한다.



// store/mutations.ts
import { RootState } from "./state";

// 뮤테이션 타입
export enum MutationTypes {
  SET_TOKEN = "SET_TOKEN",
}

// 뮤테이션 속성 함수
export const mutations = {
  [MutationTypes.SET_TOKEN](state: RootState, token: string) {
    state.token = token;
  },
}

export type Mutations = typeof mutations;


12.7 mutations 타입 정의

Note

12.7.1 mutations 타입 정의

궁금한점

// src/store/index.ts
// ...
const store: StoreOptions<RootState> = {
  state,
  mutations,
};
// ...

위에 StoreOptions에 제네릭으로 RootState 타입을 넘겨줬는데,
RootStatestate의 타입인데..
저렇게 넣으니까 mutationsRootState로 추론되는거 같고…
아닌가?
잘 모르겠다..
이 부분도 앞으로 남은 강의에서 말해주겠지?

12.8 스토어 타입 추론을 위한 타입 파일 작성

mutations 코드를 정의했다.
mutations 코드의 타입들이 컴포넌트 레벨에서 잘 추론될 수 있도록 중간에 매개 코드를 작성해보도록 하겠다.

Note

node_modules 안에 타입 추론되게 해도 모든 것에 대해서 추론되게 할 수는 없다.
예를 들어 commit - mutations을 호출하는 API인 이 commit이라던가
actions를 호출하는 dispatch 같은 것을 쓸 때
타입 추론이 자동으로 되게끔 하는 것은 어렵다.

이런 부분들이 vue2에서는 타입스크립트 추론을 생각하고 그걸 고려해서 만든 것이 아니기 때문에 이렇게 한계점이 있는 것이다.
다행히도 지금 수업을 듣는 이 시점에서는 vue3가 나와있고, 안정화 버전오 나왔고 vuex도 4점대 버전이 나와있기 때문에
안정화된 이 2개의 라이브러리들에서 현재보다 훨씬 좀 더 유연하게 타입을 확장할 수 있는 형태로 다 제공이 되고 있다.

그런 부분들을 다 참고하면 좋을 것 같다.
강의 후반부에 vue3, vuex관련해서도 말씀드릴 예정이다.

12.8.1 뷰 컴포넌트에서 활용할 수 있도록 뷰엑스 커스텀 타입 정의

글 서두에 언급한 것처럼 뷰엑스의 내부 타입 방식으로는 위에서 정의한 statemutations가 올바르게 추론되지 않습니다.
이 문제를 해결하기 위해 store/types.ts에 아래와 같이 작성합니다.

// store/types.ts
import { CommitOptions, Store } from "vuex";
import { Mutations } from "./mutations";
import { RootState } from "./state";

type MyMutations = {
    commit<K extends keyof Mutations, P extends Parameters<Mutations[K]>[1]>(
        key: K,
        payload?: P,
        options?: CommitOptions
    ): ReturnType<Mutations[K]>;
}

export type MyStore = Omit<
        Store<RootState>,
        "commit"
> & MyMutations
Note

12.8.2 뷰 컴포넌트에서 활용할 수 있도록 뷰엑스 커스텀 타입 정의

12.9 스토어 타입 파일 설명 및 스토어 내부 타입 확장

// store/types.ts
import { CommitOptions, Store } from "vuex";
import { Mutations } from "@/store/mutations";
import { RootState } from "@/store/state";

type MyMutations = {
    commit<K extends keyof Mutations, P extends Parameters<Mutations[K]>[1]>(
        key: K,
        payload?: P,
        options?: CommitOptions
    ): ReturnType<Mutations[K]>;
};

export type MyStore = Omit<Store<RootState>, "commit"> & MyMutations;

위 파일에 대해 조금 더 설명해보겠다.
types.ts 파일에서 내보내는 타입은 MyStore라는 타입 하나이다.

  • 유니온 |
  • 인터섹션 &

  • 즉 위 코드에선 OmitMyMutations의 인터섹션. 즉, 합집합
    • 앞에 있는 타입과 뒤의 타입을 합치겠다는 뜻이다.

12.9.1 인터섹션(합집합) 복습

type A = {
    name: string
}
type B = {
    age: number
}
type C = A & B

const person: C = {
    name: 'a',
    age: 10
}
  • name 또는 age 둘 중 하나라도 정의 안되어있으면 person에서 에러가 발생한다.

12.9.2 Omit 이란?

const person = {
    name: 'a',
    age: 10,
    skill: 'js'
}

const josh: Omit<person, 'skill'> = {
    name: 'a',
    age: 10,
}

Omit<person, 'skill'>person에서 skill 키를 빼고 나머지, name, age만을 가리킨다.

12.9.3 다시 코드 분석

// store/types.ts
import { CommitOptions, Store } from "vuex";
import { Mutations } from "@/store/mutations";
import { RootState } from "@/store/state";

type MyMutations = {
    commit<K extends keyof Mutations, P extends Parameters<Mutations[K]>[1]>(
        key: K,
        payload?: P,
        options?: CommitOptions
    ): ReturnType<Mutations[K]>;
};

export type MyStore = Omit<Store<RootState>, "commit"> & MyMutations;

Store<RootState> 여기서 commit만 빼고..
Store<RootState> 여기서 Storeclass로 정의가 되어있을 것이다.

export declare class Store<S> {
  constructor(options: StoreOptions<S>);

  readonly state: S;
  readonly getters: any;

  replaceState(state: S): void;

  dispatch: Dispatch;
  commit: Commit;

  subscribe<P extends MutationPayload>(fn: (mutation: P, state: S) => any, options?: SubscribeOptions): () => void;
  subscribeAction<P extends ActionPayload>(fn: SubscribeActionOptions<P, S>, options?: SubscribeOptions): () => void;
  watch<T>(getter: (state: S, getters: any) => T, cb: (value: T, oldValue: T) => void, options?: WatchOptions): () => void;

  registerModule<T>(path: string, module: Module<T, S>, options?: ModuleOptions): void;
  registerModule<T>(path: string[], module: Module<T, S>, options?: ModuleOptions): void;

  unregisterModule(path: string): void;
  unregisterModule(path: string[]): void;

  hasModule(path: string): boolean;
  hasModule(path: string[]): boolean;

  hotUpdate(options: {
    actions?: ActionTree<S, S>;
    mutations?: MutationTree<S>;
    getters?: GetterTree<S, S>;
    modules?: ModuleTree<S>;
  }): void;
}

위처럼 Storeclass로 정의가 되어있다.
위 정의에서 commit만 빼고 나머지는 전부 다 들고 오겠다. 라는 뜻이다.

12.9.4 commit만 빼는 이유?

Storecommit, dispatch 등등이 있었는데, commit만 빼는 이유는 commit에 대해서 프로젝트 레벨에서 재정의를 할 것이기 때문이다.

// store/types.ts
import { CommitOptions, Store } from "vuex";
import { Mutations } from "@/store/mutations";
import { RootState } from "@/store/state";

type MyMutations = {
    commit<K extends keyof Mutations, P extends Parameters<Mutations[K]>[1]>(
        key: K,
        payload?: P,
        options?: CommitOptions
    ): ReturnType<Mutations[K]>;
};

export type MyStore = Omit<Store<RootState>, "commit"> & MyMutations;

위 코드를 보면 인터섹션으로 MyMutations를 합쳐주고 있다.
즉, Store에다가 commit을 빼고 그 부분을 MyMutations로 갈아 끼워준다고 보시면된다.

MyMutations는 프로젝트 레벨에서 정의한 mutations의 타입이다.
새로운 mutations 타입을 갈아끼워넣어 새로운 Store 타입을 정의한다고 보면된다.

결론적으로 MyStore 타입만 다른 컴포넌트로 들고가서 활용하게되면 아까 저희가 작성했었던 mutations의 타입들을 저희가 추론할 수 있게 된다.

// node_modules/vuex/types/vue.d.ts
/**
 * Extends interfaces in Vue.js
 */

import { RootState } from "../../../src/store/state";
import { MyStore } from "../../../src/store/types";
import Vue, { ComponentOptions } from "vue";
import { Store } from "./index";

declare module "vue/types/options" {
  interface ComponentOptions<V extends Vue> {
    // store?: Store<RootState>;
    store?: MyStore;
  }
}

declare module "vue/types/vue" {
  interface Vue {
    // $store: Store<RootState>;
    store?: MyStore;
  }
}

이번 강의에서도 일단 이런식으로 node_modules 안에 라이브러리 폴더에서 수정하네..
이 방법말고 다른 방법을 알고 싶은거라고!!!
일단 위와 같이 하면 타입 추론은 잘 되는 것 같다.
하지만 위와 같은 방법말고~~~.

Note

12.9.5 스토어 타입 파일 설명 및 스토어 내부 타입 확장

12.10 뮤테이션 타입 설명 마무리 및 actions 타입 정의 안내

// store/types.ts
import { CommitOptions, Store } from "vuex";
import { Mutations } from "@/store/mutations";
import { RootState } from "@/store/state";

type MyMutations = {
  // 아래는 제네릭(타입변수)을 두개 받은 것
  // K extends keyof Mutations: Mutations의 키를 받아서 첫번째 제네릭으로..
  // Parameters<Mutations[K]>[1]: 첫번째 제네릭으로받은 Mutations의 키의 두번째 파라미터
  // 첫번째 파라미터는 state이고 두번째 파라미터가 payload
  // 즉, 두번째 파라미터인 payload를 두번째 제네릭으로..
  commit<K extends keyof Mutations, P extends Parameters<Mutations[K]>[1]>(
    key: K,
    payload?: P,
    options?: CommitOptions
  ): ReturnType<Mutations[K]>;
  // commit(MutationTypes.SET_NEWS, 10) <- 즉, 커밋을 작성할 때 이런식으로 작성할텐데,
  // 첫번째 인자로 들어간 것을 보고, 두번째 인자인 payload의 타입을 추론하기 위해, 위와 같이 타입 정의를 한 것이다.

  // CommitOptions는 기본적으로 vuex에서 제공되는 것이다.
  // ReturnType: 유틸리티 타입 - commit의 반환 타입까지 추론되도록 정의
};

// class로 정의되어있는 Store의 여러가지 속성 중에 commit을 제외
// 그리고 MyMutations에 정의한 commit을 합쳐서 새로 Store 타입을 정의
export type MyStore = Omit<Store<RootState>, "commit"> & MyMutations;

12.10.1 actions 타입 정의

actions 함수도 아래와 같이 정의할 수 있다.

// store/actions.ts
import { ActionContext } from "vuex";
import { Mutations } from "./mutations";
import { RootState } from "./state";

export enum ActionTypes {
  FETCH_NEWS = "FETCH_NEWS"
}

interface News {
  title: string;
  id: number;
}

type MyActionContext = {
  commit<K extends keyof Mutations>(
          key: K,
          payload?: Parameters<Mutations[K]>[1]
  ): ReturnType<Mutations[K]>;
} & Omit<ActionContext<RootState, RootState>, "commit">;

export const actions = {
  async [ActionTypes.FETCH_NEWS](context: MyActionContext, payload?: number) {
    const res = await fetch("https://jsonplaceholder.typicode.com/users/1");
    const user: News[] = await res.json();
    return user;
  }
};

export type Actions = typeof actions;
Note

12.10.2 actions 타입 정의

12.11 actions 타입 정의

// ...
type MyActionContext = {
  commit<K extends keyof Mutations>(
          key: K,
          payload?: Parameters<Mutations[K]>[1]
  ): ReturnType<Mutations[K]>;
} & Omit<ActionContext<RootState, RootState>, "commit">; // 사실 이렇게 하는 것보단 MutationsTree 같은 Vuex 내장 타입을 확장하는 식으로 하는게 날것같다.
// 여튼 위 코드의 뜻은 Mutations의 Context에 기본적으로 제공되는게 commit이 있을텐데,
// ActionContext의 commit을 omit으로 빼가지고
// 내가 정의한 commit과 합쳐서 MyActionContext로 제공하겠다 라는 뜻이다.
// 한마디로 저희가 정의한 mutations 타입까지 이 actions 안에서 추론되게끔 정의를 하겠다 라고 보시면됨
// ...

위 링크 보시면 MyActionContext라고 있는데, vuex 내부적으로 ActionContext라는 타입이 존재하고 있습니다.
그거를 이용해 확장한 것입니다.
저희가 이를 확장해야 되는 이유는 앞서 말씀드렸던 mutations라던가 이런게 store 내부적으로 확장이 안되는 형태로 되어 있습니다.
그래서 그런걸 저희가 커스텀해서 확장할 수 있게 재정의를 하는 것입니다.


ActionContext<CustomState, RootState>애서 오른쪽에 들어가는게 RootState이고, (이 강의에서 RootState는.. 아래..)
왼쪽에 들어가는게, CustomState같은… 모듈화된 State가 왼쪽에 위치하면 된다.
즉, 현재 State / 루트 State 이렇게 왼쪽오른쪽 넣어주면된다.

import { NewsItem } from "@/api";

const state = {
  news: [] as NewsItem[],
};

type RootState = typeof state;

export { state, RootState };

12.11 강의 처음부터 다시 듣기

  • 타입스크립트를 생각하지 않고 일단 자바스크립트로써 코드를 작성해보면 아래와 같이 작성할 수 있다.
const actions = {
  async FETCH_NEWS(context, payload) {
    await fetchNews();
  }
}
  • 앞서 배웠던 mutations 타입과 연관지어서 생각해봤을 때, enum으로 ActionTypes를 정의하면 좋을 것이다.
  • vuex에 정의되어있는 ActionContext 타입을 가지고 context 아규먼트의 타입을 정의한다.
  • 두번째 인자인 payload는 지금 단계에선 아직 의미가 없는게, 컴포넌트 단위에서 this.$store.dispatch(ActionTypes.FETCH_NEWS) 이런식으로 payload 인자 없이 호출할 것이기 때문이다.
  • 그래서 아래와 같이 ?를 붙여 옵셔널로, 그리고 any 타입으로 넣으셔도 일단 상관 없습니다.
import { ActionContext } from 'vuex';

enum ActionTypes {
  FETCH_NEWS = 'FETCH_NEWS'
}

const actions = {
  [ActionTypes.FETCH_NEWS](context: ActionContext, payload?: any) {
    
  }
}
  • 여기서 또 정의해야될 것이 context에 정의할 타입에 대해서 정의해야된다. 이는 일단 문서에 적혀있는 것을 들고와서 얘기를 해보도록 하겠다.
  • 링크
  • 문서에 보시면 아래와 같이 MyActionContext라고 정의가 되어있다.
// ...
type MyActionContext = {
  commit<K extends keyof Mutations>(
          key: K,
          payload?: Parameters<Mutations[K]>[1]
  ): ReturnType<Mutations[K]>;
} & Omit<ActionContext<RootState, RootState>, "commit">;
// ...

12.11 actions 타입 정의 2022년 6월 12일 강의 다시 듣기

타입스크립트에 내재되어있는 타입 Parameters

특정 함수의 인자 값들이 있을거아냐?
예를 들면 (a, b) => void 이런식의 함수가 있다고 해보자.
그러면 이 함수의 ab라는 인자를 가지고 해당 파라미터의 타입도 이럴 것이다! 라고 추론을 해주는 거야.

즉, arg: Parameters<(a, b) => void>[1] 이렇게 작성하면 a, b 중에 두번째 인자인 b 타입으로 해당 arg 파라미터 타입을 추론해라 이 뜻인거지.
이 개념 잘 이해해!

import { ActionContext } from "vuex";
import { Mutations, MutationTypes } from "@/store/mutations";
import { RootState } from "@/store/state";
import { fetchNews } from "@/api";

enum ActionTypes {
  FETCH_NEWS = "FETCH_NEWS",
}

type MyActionContext = {
  // commit을 재정의
  // K는 Mutations에서 확장된 것. 즉 Mutations에 포함되어있는 함수
  // payload는 Mutations의 K함수의 2번째(1) 인자여야 된다는 뜻
  // Return 타입은 Mutations의 K 함수의 리턴값과 동일하다는 뜻
  commit<K extends keyof Mutations>(
    key: K,
    payload: Parameters<Mutations[K]>[1]
  ): ReturnType<Mutations[K]>;
} & Omit<ActionContext<RootState, RootState>, "commit">;
// MyActionContext 타입은 ActionContext의 commit을 없애고, 위의 commit과 합친 타입이라는 뜻
// ActionContext<RootState, RootState>
// 모듈화를 하신다면, 오른쪽에 들어가는게 RootState이고 왼쪽에 들어가는게 JobsState 같은 모듈화된 State가 들어간다.

const actions = {
  // 두번째 인자인 payload는 컴포넌트 레벨에서 어차피 사용하지 않을 것이라 optional에 any 타입을 부여했다.
  // 첫번째 인자인 context에 대한 타입을 정의해보도록 하겠다.
  // vuex 내부적으로 ActionContext라는 타입이 존재한다.
  // 이를 확장해야되는 이유는 앞서 말씀드렸던 것처럼 mutations나 이런 것들이 store 내부적으로 확장이 안되는 형태로 되어있기 때문이다.
  // 이를 극복하기 위한 한가지 방법이라고 생각하고 보면된다.
  async [ActionTypes.FETCH_NEWS](context: MyActionContext, payload?: any) {
    const { data } = await fetchNews();
    context.commit(MutationTypes.SET_NEWS, data);
  },
};

type Actions = typeof actions;

export { ActionTypes, actions, Actions };

12.12 스토어 타입 파일에 actions 타입 확장

타입정의를 했지만 추론이 잘 안된다..
내가 뭘 잘못 따라한건지..
타입스크립트 버전업이되고 vue2의 한계인건지..
여튼 잘 안됨.

아.. 중간에 node_modules 안에 수정 이거 안해서그렇구나.
중간 강의처럼 node_modules 안에 내용을 직접 수정해줘야 타입추론이 되는거였는데.
이렇게하면 협업에 문제가 있어서 일단 그냥 보자 라고 이건 안따라고 봤었지 참..

여튼 지금까지는 타입정의해도 node_modules 폴더 내에 vuex 라이브러리 안에 타입추론을 직접 손보지 않으면 타입추론이 안되는 방법이었음.
일단 이 다음 시간부터는 node_modules를 안 건드리고 타입추론되게끔 하는 방법에 대해서 진행

import { CommitOptions, DispatchOptions, Store } from "vuex";
import { Mutations } from "@/store/mutations";
import { RootState } from "@/store/state";
import { Actions } from "@/store/actions";

type MyMutations = {
  // 아래는 제네릭(타입변수)을 두개 받은 것
  // K extends keyof Mutations: Mutations의 키를 받아서 첫번째 제네릭으로..
  // Parameters<Mutations[K]>[1]: 첫번째 제네릭으로받은 Mutations의 키의 두번째 파라미터
  // 첫번째 파라미터는 state이고 두번째 파라미터가 payload
  // 즉, 두번째 파라미터인 payload를 두번째 제네릭으로..
  commit<K extends keyof Mutations, P extends Parameters<Mutations[K]>[1]>(
    key: K,
    payload?: P,
    options?: CommitOptions
  ): ReturnType<Mutations[K]>;
  // commit(MutationTypes.SET_NEWS, 10) <- 즉, 커밋을 작성할 때 이런식으로 작성할텐데,
  // 첫번째 인자로 들어간 것을 보고, 두번째 인자인 payload의 타입을 추론하기 위해, 위와 같이 타입 정의를 한 것이다.

  // CommitOptions는 기본적으로 vuex에서 제공되는 것이다.
  // ReturnType: 유틸리티 타입 - commit의 반환 타입까지 추론되도록 정의
};

// MyActions에서 dispatch 정의
type MyActions = {
  // Actions 타입의 key에서 확장한.. 즉, actions에 속한 함수를 K라고 정의
  dispatch<K extends keyof Actions>(
    key: K, // Actions의 함수를 첫번째 인자 key로 정의
    payload?: Parameters<Actions[K]>[1], // Actions의 함수의 2번째 인자를 이 payload 인자로 정의
    options?: DispatchOptions // 옵션은 vuex에 내재되어있는 DispatchOptions로 정의
  ): ReturnType<Actions[K]>; // 리턴 타입은 Actions의 함수반환값으로 정의
};

// class로 정의되어있는 Store의 여러가지 속성 중에 commit을 제외
// 그리고 MyMutations에 정의한 commit을 합쳐서 새로 Store 타입을 정의
// Store의 commit과 dispatch 제거
export type MyStore = Omit<Store<RootState>, "commit" | "dispatch"> &
  MyMutations &
  MyActions;

12.13 커스텀 타입을 프로젝트 레벨로 설정하는 방법

  • node_modules/vuex/types/vud.d.ts

위 파일을 보시면

/**
 * Extends interfaces in Vue.js
 */

import Vue, { ComponentOptions } from "vue";
import { Store } from "./index";

declare module "vue/types/options" {
  interface ComponentOptions<V extends Vue> {
    store?: Store<any>;
  }
}

declare module "vue/types/vue" {
  interface Vue {
    $store: Store<any>;
  }
}

위와 같이 되어있는데, Extends interfaces in Vue.js 이런 문구가 있다.
즉, vue.js 내부적으로 Vue 인터페이스가 정의가 되어있을 것이고 그 인터페이스를 확장하는 방식이

interface Vue {
  $store: Store<any>;
}

이 부분이다라고 말씀을 드렸었습니다.

수정 후

/**
 * Extends interfaces in Vue.js
 */

import { RootState } from '../../../src/store/state';
import { MyStore } from '../../../src/store/types';
import Vue, { ComponentOptions } from "vue";
import { Store } from "./index";

declare module "vue/types/options" {
  interface ComponentOptions<V extends Vue> {
    // store?: Store<any>;
    store?: MyStore;
  }
}

declare module "vue/types/vue" {
  interface Vue {
    // $store: Store<any>;
    $store: MyStore;
  }
}

위 방법의 문제점

  1. node_modules 폴더를 재설치하면 위와 같이 수정한 내용은 다 사라지게된다. 관리가 어렵다.
  2. 다른 사람과 위 코드를 공유하기 어렵다. 깃으로 형상관리가 되질 않는다. node_modules는 깃 형상관리에서 제외하기 때문에. 추적이 안된다.

해결방법

  • node_modules/vuex/types/vud.d.ts 이 파일을 프로젝트 레벨에 src/project.d.ts 파일로 꺼내서 설정하면 된다.
// src/project.d.ts
import Vue from "vue";
import { MyStore } from "@/store/types";

declare module "vue/types/vue" {
  interface Vue {
    $store: MyStore;
  }
}

declare module "vue/types/options" {
  interface ComponentOptions<V extends Vue> {
    store?: MyStore;
  }
}

src/project.d.ts 파일을 현재 타입스크립트 프로젝트에서 인식할 수 있는 형태로 설정만 추가해주시면 끝납니다.

12.14 뷰엑스 커스텀 타입을 프로젝트 레벨에 설정

  • src/project.d.ts 파일이 현재 타입스크립트 프로젝트에서 잘 인식되도록 tsconfig.json 파일을 수정하도록 하겠습니다.
{
  "include": [
    "src/**/*.ts",
    "src/**/*.tsx",
    "src/**/*.vue",
    "tests/**/*.ts",
    "tests/**/*.tsx",
    "src/project.d.ts"
  ]
}

이렇게 src/project.d.ts로 추가해도되는데,

  1. 외부 라이브러리 모듈화라던지
  2. 뷰 플러그인을 살펴보게 될텐데,

그랬을 때 위와 같이 타입들에 대한 정의들을 조금 더 추가해나갈 예정이다.
때문에 아래와 같이 src 폴더에 types라는 타입 정의 폴더를 만들고, 거기에 이 뷰엑스 관련 커스텀 타입 뿐만아니라, 기타 타입까지 설정할 수 있도록 해보겠습니다.

{
  "include": [
    "src/**/*.ts",
    "src/**/*.tsx",
    "src/**/*.vue",
    "tests/**/*.ts",
    "tests/**/*.tsx",
    "src/types/**/*.d.ts"
  ]
}

앞으로 src/types 폴더 안에있는 타입 정의 파일을 타입스크립트에서 인식한다.

그런데 여전히 node_modules/vuex/types/vue.d.ts 파일을 위와 같이 수정 해야지 타입추론이 제대로된다.
이미 프로젝트 레벨 수준에 src/types/project.d.ts 파일을 생성해 타입을 정의했는데, 이 파일이 제대로 인식되려면 어떻게 해야될까?
node_modules/vuex/types/vue.d.ts 파일을 지우면된다.

으음.. 번거롭네..

여튼 주석으로도 노티를 해주는 것이 좋다.

// src/project.d.ts
// NOTE: node_modules/vuex/types/vue.d.ts 파일을 삭제해줘야 아래 타입이 정상 추론됨
// 지우지 않으면 vue2에서는 추론 자체를 node_modules/vuex/types/vue.d.ts 를 먼저 물기 때문에 프로젝트 레벨에서 설정한 것들이 잘 추론되지 않는다.
import Vue from "vue";
import { MyStore } from "@/store/types";

declare module "vue/types/vue" {
  interface Vue {
    $store: MyStore;
  }
}

declare module "vue/types/options" {
  interface ComponentOptions<V extends Vue> {
    store?: MyStore;
  }
}
  • vue2에선 번거롭겠지만 이런식으로 해야 타입 추론이 잘 된다.

12.15 타입 모듈 확장 방법

{
  "include": [
    "src/**/*.ts",
    "src/**/*.tsx",
    "src/**/*.vue",
    "tests/**/*.ts",
    "tests/**/*.tsx",
    "src/types/**/*.d.ts"
  ]
}

tsconfig.json에 선언파일을 타입스크립트에서 인식할 수 있게 위와 같이 수정을 해봤었다.
include 속성에 경로를 추가해서 인식할 수 있게 만들었는데, 이 방식은 뷰의 기능이라기보단
타입스크립트에서 원래 모듈을 읽어오는 방법, 방식에 기인한 것이라고 보면된다.

타입스크립트가 모듈을 어떻게 읽어와서 확장을 시켰는지 간단한 원리를 알려드리겠다.

위 문서를 읽어보시면 된다. (타입스크립트 공식문서)
모듈 확장에 대한 개념이다.

// observable.ts
export class Observable<T> {
  // ... implementation left as an exercise for the reader ...
}

예시 코드이다.
타입스크립트 파일이지만, 자바스크립트 파일에 코드를 정의해놓은거라고 보면 된다.
특정 클래스를 정의해놓고 export 했다고 생각하면된다.

// map.ts
import { Observable } from "./observable";
Observable.prototype.map = function (f) {
  // ... another exercise for the reader
};

map.ts에서 export한 클래스를 가지고 왔다.
해당 클래스의 prototypemap을 정의를 했다.

자바스크립트의 prototype으로 내장 메소드 또는 속성을 정의할 수 있는데, 일반적인 자바스크립트의 클래스, 모듈, 프로토타입 문법이라고 보면된다.
여기서 말하고자 하는 것은 타입스크립트에 대해 말하기 전에 자바스크립트에서 이미 기존에 정의된 클래스라던가..

사실 변수나 함수는 이뮤터블이니깐. 이는 바꿀 수가 없고..

클래스 같은 경우엔 위와 같이 prototype을 이용해서 객체에 성질을 조금 변화시킬 수 있었다.

여기까지는 자바스크립트 레벨


타입스크립트 레벨에서 위와 같이 모듈을 인식을 해올 때.. 위의 예시로는 Objservable인데, 이 모듈을 import를 해올 때, 확장을 하고싶다고 한다면,

아래와 같은 방식을 채택하면 된다.

// observable.ts : 기본 정의된 타입
export class Observable<T> {
  // ... implementation left as an exercise for the reader ...
}

// map.ts : 타입 확장
import { Observable } from "./observable";
declare module "./observable" { // <- 모듈에 대한 인터페이스 정의를 추가로 해준 것을 볼 수 있다. 이런식으로 미리 정의된 타입을 확장한다.
  interface Observable<T> {
    map<U>(f: (x: T) => U): Observable<U>;
  }
}
Observable.prototype.map = function (f) {
  // ... another exercise for the reader
};

// consumer.ts : 확장된 타입을 가져다 쓰는, 실사용 예시
import { Observable } from "./observable";
import "./map";
let o: Observable<number>;
o.map((x) => x.toFixed());

vuex 타입 추론도 이와 같은 원리로 타입 확장을 하여 정의했기 때문에 가능한 것이다.
다만 아래는 타입 확장이라기보단 타입 재정의가 맞는듯?
그래서 node_modules에서 기존 파일을 삭제해줘야 타입이 정산 추론되는 것이다.

// src/project.d.ts
// NOTE: node_modules/vuex/types/vue.d.ts 파일을 삭제해줘야 아래 타입이 정상 추론됨
// 지우지 않으면 vue2에서는 추론 자체를 node_modules/vuex/types/vue.d.ts 를 먼저 물기 때문에 프로젝트 레벨에서 설정한 것들이 잘 추론되지 않는다.
import Vue from "vue";
import { MyStore } from "@/store/types";

declare module "vue/types/vue" {
  interface Vue {
    $store: MyStore;
  }
}

declare module "vue/types/options" {
  interface ComponentOptions<V extends Vue> {
    store?: MyStore;
  }
}

여튼 이러한 모듈 확장을 통해 타입 추론을 가능하게 하는 것이다.

12.16 getters 타입 정의 안내

Vuex 향후 로드맵에 맵 헬퍼 함수가 제거될 거라고 공지가 되어 있습니다.
타입스크립트 추론 관점에서도 맵 핼퍼 함수는 더 이상 사용하지 않을 것을 권장합니다.

getters도 다른것과 동일하게 store 폴더 밑에 getters.ts 파일을 만들고 해당 파일 안에 해당 코드에 대한 타입을 추론할 수 있도록 타입 정의를 해주면 된다.

// store/getters.ts
import { RootState } from './state';

export const getters = {
    getToken(state: RootState) {
        return state.token + '!';
    }
}

export type Getters = typeof getters;

그리고 exportgetters 파일을 스토어 커스텀 파일에 아래와 같이 추가합니다.

type MyGetters = {
    getters: {
        [K in keyof Getters]: ReturnType<Getters[K]>;
    }
}
// getters는 computed 속성과 동일하게 생각하시면 됩니다.
// 컴포넌트 상에서 computed로 쓰실 것을 getters로 가져와서 쓰신다고 생각하시면 됩니다.
// 예전에는 state를 꺼내서 쓸 땐 getters를 쓰세요 라고 했는데,
// 그렇게 하려면 getters를 설정하는 코드도 많이 생기기 때문에
// 굳이 getters를 사용할 필요는 없다. 만약 state 값 그대로 꺼내올거라면..
// this.$store.state.news 이렇게 접근하는 걸 추천한다.

굳이 getters로 한번 감싸서 꺼내시지 마시고, state로 바로 접근해 꺼내시는걸 추천드린다.

  • 컴포넌트에서 접근하는 state는 컴포넌트에서 변환할 수 없다.
    • this.$store.state.news = 10 이런건 하면 안된다.
    • 이렇게 코드를 작성하면 vue에서 에러를 발생시킬 것이다.
    • state 값은 항상 mutations로 바꿔야한다.

12.17 스토어 타입 파일에 getters 타입 확장

// 스토어의 getters는 선택적인 부분이다.
// 단순히 state.news; 이런식의 state 값을 꺼내오는 용도라면 getters를 사용할 필욘 없다.
// 예전에 강의할 땐, 위와 같이 단순하게 꺼내와도 computed 효과가 있으니 이렇게 꺼내오는게 맞다고 했지만,
// 반대로 코드를 불리는 원인이 되기도하기 때문에
// this.$store.state.news 이런식으로 사용하는 것을 추천드린다.
import { CommitOptions, DispatchOptions, Store } from "vuex";
import { Mutations } from "@/store/mutations";
import { RootState } from "@/store/state";
import { Actions } from "@/store/actions";
import { Getters } from "@/store/getters";

type MyMutations = {
  // 아래는 제네릭(타입변수)을 두개 받은 것
  // K extends keyof Mutations: Mutations의 키를 받아서 첫번째 제네릭으로..
  // Parameters<Mutations[K]>[1]: 첫번째 제네릭으로받은 Mutations의 키의 두번째 파라미터
  // 첫번째 파라미터는 state이고 두번째 파라미터가 payload
  // 즉, 두번째 파라미터인 payload를 두번째 제네릭으로..
  commit<K extends keyof Mutations, P extends Parameters<Mutations[K]>[1]>(
    key: K,
    payload?: P,
    options?: CommitOptions
  ): ReturnType<Mutations[K]>;
  // commit(MutationTypes.SET_NEWS, 10) <- 즉, 커밋을 작성할 때 이런식으로 작성할텐데,
  // 첫번째 인자로 들어간 것을 보고, 두번째 인자인 payload의 타입을 추론하기 위해, 위와 같이 타입 정의를 한 것이다.

  // CommitOptions는 기본적으로 vuex에서 제공되는 것이다.
  // ReturnType: 유틸리티 타입 - commit의 반환 타입까지 추론되도록 정의
};

// MyActions에서 dispatch 정의
type MyActions = {
  // Actions 타입의 key에서 확장한.. 즉, actions에 속한 함수를 K라고 정의
  dispatch<K extends keyof Actions>(
    key: K, // Actions의 함수를 첫번째 인자 key로 정의
    payload?: Parameters<Actions[K]>[1], // Actions의 함수의 2번째 인자를 이 payload 인자로 정의
    options?: DispatchOptions // 옵션은 vuex에 내재되어있는 DispatchOptions로 정의
  ): ReturnType<Actions[K]>; // 리턴 타입은 Actions의 함수반환값으로 정의
};

// 스토어의 getters는 선택적인 부분이다.
// 단순히 state.news; 이런식의 state 값을 꺼내오는 용도라면 getters를 사용할 필욘 없다.
// 예전에 강의할 땐, 위와 같이 단순하게 꺼내와도 computed 효과가 있으니 이렇게 꺼내오는게 맞다고 했지만,
// 반대로 코드를 불리는 원인이 되기도하기 때문에
// this.$store.state.news 이런식으로 사용하는 것을 추천드린다.
type MyGetters = {
  getters: {
    [K in keyof Getters]: ReturnType<Getters[K]>;
  };
};

// class로 정의되어있는 Store의 여러가지 속성 중에 commit을 제외
// 그리고 MyMutations에 정의한 commit을 합쳐서 새로 Store 타입을 정의
// Store의 commit과 dispatch 제거
export type MyStore = Omit<
  Store<RootState>,
  "commit" | "dispatch" | "getters"
> &
  MyMutations &
  MyActions &
  MyGetters;

12.18 getters 커스텀 타입 설명 및 맵드 타입 복습

  • 맵드 타입 활용해 아래와 같이 타입 적용
  • 조금 더 쉽게 이해하려면 type A = keyof Getters를 하고 A에 어떤 값이 추론되어 들어오는지 보면된다.
type MyGetters = {
  getters: {
    [K in keyof Getters]: ReturnType<Getters[K]>;
  };
};

12.19 스토어 타입 정의 방법 요약 정리

  • node_modules/vuex/types/vue.d.ts 파일 안에 직접 타입 정의
// src/store/index.ts
import Vue from "vue";
import Vuex, { StoreOptions } from "vuex";
import { RootState, state } from "@/store/state";
import { mutations } from "@/store/mutations";
import { actions } from "@/store/actions";

Vue.use(Vuex);

const store: StoreOptions<RootState> = {
  state,
  mutations,
  actions,
};

export default new Vuex.Store(store);
// src/store/state.ts
import { NewsItem } from "@/api";

const state = {
  news: [] as NewsItem[],
};

type RootState = typeof state;

export { state, RootState };
  • state까진 이렇게 타입 정의를 하고, mutations부터는 타입 정의 패턴이 달라졌다.
// src/store/mutations.ts
import { RootState } from "@/store/state";
import { NewsItem } from "@/api";

// 아래와 같이 상수화를 시키는 이유
// 1. 타입 스크립트 추론 혜택을 좀 더 받기 위해 enum 사용
// 2. 코드량이 많아지면 많아질 수록 생산성이 높아진다. (타입스크립트 자동완성 효과 + 수정사항이 있을 때 일일이 수정할 필요 없음)
enum MutationTypes {
  SET_NEWS = "SET_NEWS",
}

const mutations = {
  [MutationTypes.SET_NEWS](state: RootState, news: NewsItem[]) {
    state.news = news;
  },
};

type Mutations = typeof mutations;

export { MutationTypes, mutations, Mutations };
  • 프로젝트 레벨에서 작성하는 코드들이 자동으로 추론되게끔 아래와 같이 MyStore 타입 정의
  • gettersactions도 동일하게 정의
// src/store/types.ts
import { CommitOptions, DispatchOptions, Store } from "vuex";
import { Mutations } from "@/store/mutations";
import { RootState } from "@/store/state";
import { Actions } from "@/store/actions";
import { Getters } from "@/store/getters";

type MyMutations = {
  // 아래는 제네릭(타입변수)을 두개 받은 것
  // K extends keyof Mutations: Mutations의 키를 받아서 첫번째 제네릭으로..
  // Parameters<Mutations[K]>[1]: 첫번째 제네릭으로받은 Mutations의 키의 두번째 파라미터
  // 첫번째 파라미터는 state이고 두번째 파라미터가 payload
  // 즉, 두번째 파라미터인 payload를 두번째 제네릭으로..
  commit<K extends keyof Mutations, P extends Parameters<Mutations[K]>[1]>(
          key: K,
          payload?: P,
          options?: CommitOptions
  ): ReturnType<Mutations[K]>;
  // commit(MutationTypes.SET_NEWS, 10) <- 즉, 커밋을 작성할 때 이런식으로 작성할텐데,
  // 첫번째 인자로 들어간 것을 보고, 두번째 인자인 payload의 타입을 추론하기 위해, 위와 같이 타입 정의를 한 것이다.

  // CommitOptions는 기본적으로 vuex에서 제공되는 것이다.
  // ReturnType: 유틸리티 타입 - commit의 반환 타입까지 추론되도록 정의
};

// MyActions에서 dispatch 정의
type MyActions = {
  // Actions 타입의 key에서 확장한.. 즉, actions에 속한 함수를 K라고 정의
  dispatch<K extends keyof Actions>(
          key: K, // Actions의 함수를 첫번째 인자 key로 정의
          payload?: Parameters<Actions[K]>[1], // Actions의 함수의 2번째 인자를 이 payload 인자로 정의
          options?: DispatchOptions // 옵션은 vuex에 내재되어있는 DispatchOptions로 정의
  ): ReturnType<Actions[K]>; // 리턴 타입은 Actions의 함수반환값으로 정의
};

// 스토어의 getters는 선택적인 부분이다.
// 단순히 state.news; 이런식의 state 값을 꺼내오는 용도라면 getters를 사용할 필욘 없다.
// 예전에 강의할 땐, 위와 같이 단순하게 꺼내와도 computed 효과가 있으니 이렇게 꺼내오는게 맞다고 했지만,
// 반대로 코드를 불리는 원인이 되기도하기 때문에
// this.$store.state.news 이런식으로 사용하는 것을 추천드린다.
type MyGetters = {
  getters: {
    [K in keyof Getters]: ReturnType<Getters[K]>;
  };
};

// class로 정의되어있는 Store의 여러가지 속성 중에 commit을 제외
// 그리고 MyMutations에 정의한 commit을 합쳐서 새로 Store 타입을 정의
// Store의 commit과 dispatch 제거
export type MyStore = Omit<
        Store<RootState>,
        "commit" | "dispatch" | "getters"
        > &
        MyMutations &
        MyActions &
        MyGetters;
// tsconfig.json
{
  "include": [
    "src/**/*.ts",
    "src/**/*.tsx",
    "src/**/*.vue",
    "tests/**/*.ts",
    "tests/**/*.tsx",
    "src/types/**/*.d.ts"
  ]
}
  • include: 타입스크립트가 모듈을 해석하는 또는 파일들을 해석할 때 참고하는 대상 경로이다.
// src/types/project.d.ts
import Vue from "vue";
import { MyStore } from "@/store/types";

// NOTE: node_modules/vuex/types/vue.d.ts 파일을 삭제해줘야 아래 타입이 정상 추론됨
declare module "vue/types/vue" {
  interface Vue {
    $store: MyStore;
  }
}

declare module "vue/types/options" {
  interface ComponentOptions<V extends Vue> {
    store?: MyStore;
  }
}
  • Module Augmentation 모듈 확장 기법이라고해서 위와 같이 declare module을 통해 $store, store를 확장..보단 재정의 해줬다고 보는게 좋을듯?
  • 현재 Vue2 기준으로는 node_modules/vuex/types/vue.d.ts 파일을 삭제해줘야한다.
  • 삭제해줘야지 타입스크립트가 타입추론할 때, 프로젝트 레벨에 정의한 project.d.ts 파일을 참고하게 된다.