typescript는 javascript 와는 다르게 정해져 있는 타입 아래에서만 움직여야 하고 대부분의 오브젝트들의 타입들을 정의해줘야 합니다. vuex를 사용하면서 해당 패턴들에 관련된 타입정의를 진행하지 않으면 오류가 나는 부분을 확인했고 첫 세팅으로 nuxt.config.js 파일을 ts 파일로 변환하였습니다.
후에 vue를 쭉 살펴보니 vuex 3.6.2 버전에서는 vuex(상태 관리를 위한 패턴이자 라이브러리) 패턴들에 대한 typescript의 타입 추론을 제공해주지 않는 상태였고(store에 대한 타입추론들이 모두 any로 되어있음, 타입추론이 안됨) 이 부분 관련해서 찾아본 두 가지 방법이 있었습니다.
vue-decorator-property 사용의 단점
vue.extend 문법의 단점
저희 팀은 후자를 선택하게 되었습니다. 다른 외부 라이브러리에 관해 의존성이 높은 걸 선호하지 않았기 때문입니다.
vue.extend를 적용하기 위해서 node-moduel에서 자동으로 생성해준 vue.d.ts 파일을 제거하고 직접 d.ts 파일을 생성해서 아래와 같이 적용해주었습니다.
store/index.ts
import { myRootState } from '@/store/state'
import { memberState } from '@/store/member/memberState'
import { smsState } from '@/store/sms/smsState'
export const state = {
...myRootState, ...memberState, ...smsState
}
store/state.ts
import { Cookies } from '~/data/enums'
const cookiesObj = {
[Cookies.accessToken]: '',
// ...
}
const myRootState = {
host: '' as string,
// ...
};
type MyRootState = typeof myRootState;
export { MyRootState, myRootState };
이렇게 까지만 하면 타입 추론이 안되고 node-module 에서 vuex의 vue.d.ts 파일을 제거하고 직접 타입 추론 파일을 제작, 설정해야지 타입 추론이 이루어질 수 있습니다.
store/types.ts
// 인터섹션 (합진합) -> &
export type MyStore = Omit<
Store<MyRootState> &
Store<SmsState> &
Store<SendHistoryState> &
Store<MemberState>,
"commit" | "dispatch" | "getters"
> &
MyMutations &
MySmsMutations &
MyMemberMutations &
MySendHistoryMutations &
MyActions &
MySmsActions &
MyMemberActions &
MySendHistoryActions &
MyGetters;
해당 파일에서 vuex의 재정의를 Omit에서 적용시켜주었습니다.
위 코드에서는 state에 타입을 씌워 타입 추론이 되게 하는 방법입니다. (vue3가 되면 코어에 타입스크립트를 잘 씌울 수 있게 대응해 줬기 때문에 vue2에서는 이렇게 한다는 느낌으로다가 그냥 가볍게 보면 됩니다. )
mutation도 역시 vuex의 vue.d.ts에 타입을 알려주지 않는 이상 타입 추론이 일어나지 않습니다. mutations의 타입을 곁들인 타입 파일이 필요한 시점입니다.
data/enums/mutationTypes.ts
export enum MutationTypes {
SET_HOST = "SET_HOST",
}
store/baseMutations.ts
import Vue from 'vue'
import { MutationTypes } from '~/data/enums/mutationTypes'
import { MyRootState } from '~/store/state'
import { CookieParams, RemoveCookieParams } from '~/data/interface'
const rootMutations = {
[MutationTypes.LOADING](state: MyRootState, isLoading: boolean) {
Vue.set(state, 'isLoading', isLoading);
},
type Mutations = typeof rootMutations
export { rootMutations, Mutations }
여기서 [MutationTypes.LOADING]
-> 이 부분 좀 의아해 할 수 있는 부분인데, 상수화하는게 나중에 타입추론에서 이점을 얻을 수 있기때문에 상수화를 했고, 공식문서에서도 추천하고 있는 방법입니다. ([]이 문법은 es 6의 computed property name)
import { rootMutations } from '@/store/baseMutations'
import { smsMutations } from '@/store/sms/smsMutations'
import { sendHistoryMutations } from '@/store/sendHistory/sendHistoryMutations'
export const mutations = {
...rootMutations, ...smsMutations, ...memberMutations, ...sendHistoryMutations
}
store/member/memberActions.ts
type MyMemberActionContext = {
commit<K extends keyof MemberMutations>(
key: K,
payload?: Parameters<MemberMutations[K]>[1]
): ReturnType<MemberMutations[K]>;
} & Omit<ActionContext<MemberState, MyRootState>, "commit">;
Omit<ActionContext<MemberState, MyRootState>, “commit”> & MyRootState -> 제네릭의 두 번째 param “commit”의 제외한 나머지 타입을 전부 받아들이고 내가 정의한 MyMutations가 commit 타입으로써 정의되는 것입니다.. 이제 MyStore를 삽입시키면 됩니다.
store/index.ts
import { rootMutations } from '@/store/baseMutations'
import { smsMutations } from '@/store/sms/smsMutations'
import { sendHistoryMutations } from '@/store/sendHistory/sendHistoryMutations'
export const mutations = {
...rootMutations, ...smsMutations, ...memberMutations, ...sendHistoryMutations
}
내가 정의한 commit의 타입이 잘 읽어지고 있습니다..이런식으로 나머지 타입도 타입 정의를 시켜줍니다.
data/enums/actionTypes/ts
export enum ActionTypes {
LOGIN = 'LOGIN',
// ...
}
store/baseActions.ts
import { ActionContext } from "vuex";
import { MyRootState } from "~/store/state";
import { Cookies } from "~/data/enums";
import { ActionTypes } from "~/data/enums/actionTypes";
import { MutationTypes } from "~/data/enums/mutationTypes";
import { Mutations } from "~/store/baseMutations";
type MyActionContext = {
commit<K extends keyof Mutations>(
key: K,
payload?: Parameters<Mutations[K]>[1]
): ReturnType<Mutations[K]>;
} & Omit<ActionContext<MyRootState, MyRootState>, "commit">;
const rootActions = {
nuxtServerInit(
vuexContext: MyActionContext,
{ req }: any
) {
// ...
}
},
};
type BaseActions = typeof rootActions;
export { rootActions, BaseActions };
store/index.ts
import { rootActions } from '~/store/baseActions'
import { memberActions } from '~/store/member/memberAction'
import { smsActions } from '~/store/sms/smsActions'
export const actions = {
...rootActions, ...memberActions, ...smsActions, ...sendHistoryActions
}
store/types.ts
import { CommitOptions, DispatchOptions, Store } from "vuex";
import { Mutations } from "./baseMutations";
import { BaseActions } from '~/store/baseActions';
import { Getters } from '~/store/getters';
//...another
type MyMutations = {
commit<K extends keyof Mutations, P extends Parameters<Mutations[K]>[1]>(
key: K,
payload?: P,
options?: CommitOptions
): ReturnType<Mutations[K]>;
};
type MyActions = {
dispatch<K extends keyof BaseActions>(
key: K,
payload?: Parameters<BaseActions[K]>[1],
options?: DispatchOptions
): ReturnType<BaseActions[K]>;
};
// ...another
지금까지 한 타입들을 정리하여 store/types.ts에 정의, 내 Store의 타입을 정의합니다.
types/project.d.ts
import Vue from "vue";
import { MyStore } from "~/store/types";
// TODO: `node_module/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;
}
타입정의파일을 만들어 tsconfig.json에 내가 정의한 타입정의파일을 포함시켜 type intelligence가 읽어들이게 해줍니다.
node_modules/vuex/type/vue.d.ts 파일을 삭제시켜주면 내가 정의한 project.d.ts로 $store의 타입 추론이 이루어지는걸 알수있습니다.
읽어주셔서 감사합니다 🙇♀️