Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

typescript enhanced: better Intelligent perception and TS checks #2054

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
62 changes: 48 additions & 14 deletions docs/zh/guide/typescript-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,23 @@
Vuex 提供了类型声明,因此可以使用 TypeScript 定义 store,并且不需要任何特殊的 TypeScript 配置。请遵循 [Vue 的基本 TypeScript 配置](https://v3.cn.vuejs.org/guide/typescript-support.html)来配置项目。

但是,如果你使用 TypeScript 来编写 Vue 组件,则需要遵循一些步骤才能正确地为 store 提供类型声明。
## store 类型推断
使用`createStore` 方法创建一个store实例时将推断出完整的 store 类型定义,支持 commit、dispatch 方法的参数感知及类型检查。

在定义 state 对象属性时推荐使用 `class` 来进行类型声明及指定默认值;

```ts
class State {
name = ''
count = 1
foo?: string
list?: string[] = []
}
const store = createStore({
state: new State(),
...
}
```

## Vue 组件中 `$store` 属性的类型声明

Expand All @@ -12,19 +29,38 @@ Vuex 没有为 `this.$store` 属性提供开箱即用的类型声明。如果你

```ts
// vuex.d.ts
import { ComponentCustomProperties } from 'vue'
import { Store } from 'vuex'
// 引入定义好的store实列
import { store } from './store'

declare module '@vue/runtime-core' {
// 声明自己的 store state
interface State {
count: number

// 为 `this.$store` 提供类型声明
interface ComponentCustomProperties {
$store: typeof store
}
}
```
## 使用 `inject` 组合式函数类型声明
将 store 安装到 Vue 应用时,会同时将 store 注入为应用级依赖,在未指定 `InjectionKey` 时将使用 "store" 作为默认 key, 因此我们可以在组合式 API 中使用`inject('store')`来拿到 store 实例,但是却无法感知返回的数据类型,为此我们同样可以使用上面的方式给 `inject` 方法添加一个重载类型补充:

```ts
// vuex.d.ts
// 引入定义好的store实列
import { store } from './store'

// 指定需要全局声明的依赖类型
interface InjectionMap {
'store': typeof store
}

declare module '@vue/runtime-core' {

// 为 `this.$store` 提供类型声明
interface ComponentCustomProperties {
$store: Store<State>
$store: InjectionMap[S]
}
// 将 injectionMap 中的类型进行重载补充声明
export function inject<S extends keyof InjectionMap>(key:S):InjectionMap[S]
}
```

Expand All @@ -44,18 +80,16 @@ import { InjectionKey } from 'vue'
import { createStore, Store } from 'vuex'

// 为 store state 声明类型
export interface State {
count: number
class State {
count: number = 0
}

// 定义 injection key
export const key: InjectionKey<Store<State>> = Symbol()

export const store = createStore<State>({
state: {
count: 0
}
state: new State()
})

// 定义 injection key
export const key: InjectionKey<typeof store> = Symbol()
```

然后,将 store 安装到 Vue 应用时传入定义好的 injection key。
Expand Down
70 changes: 60 additions & 10 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@ import { createLogger } from "./logger";
export * from "./helpers";
export * from "./logger";

export declare class Store<S> {
constructor(options: StoreOptions<S>);
export declare class Store<S, Opt extends StoreOptions<S> = Obj > {
constructor(options: Opt);

readonly state: S;
readonly getters: any;

install(app: App, injectKey?: InjectionKey<Store<any>> | string): void;

replaceState(state: S): void;

dispatch: Dispatch;
commit: Commit;
readonly getters: AllGetter<Opt>
readonly state: AllState<Opt>
dispatch: AllDispatch<Opt>
commit: AllCommit<Opt>

subscribe<P extends MutationPayload>(fn: (mutation: P, state: S) => any, options?: SubscribeOptions): () => void;
subscribeAction<P extends ActionPayload>(fn: SubscribeActionOptions<P, S>, options?: SubscribeOptions): () => void;
Expand All @@ -44,10 +44,60 @@ export declare class Store<S> {
}

export const storeKey: string;

export function createStore<S>(options: StoreOptions<S>): Store<S>;

export function useStore<S = any>(injectKey?: InjectionKey<Store<S>> | string): Store<S>;
type ExStoreOptions<Opt extends StoreOptions<any>, S> = StoreOptions<S> & Opt

export function createStore<S, Opt>(options: ExStoreOptions<Opt, S>): Store<S,Opt>;

export function useStore<S = any>(injectKey?: InjectionKey<S> | string): S;
type Obj<T = any> = Record<string, T>

type AddPrefix<Keys, Prefix = ''> = `${Prefix & string}${Prefix extends '' ? '' : '/'}${Keys & string}`
type AddNs<K, P, N> = N extends { namespaced: boolean } ? (N['namespaced'] extends true ? AddPrefix<K, P> : P) : P
type GetParam<F> = F extends (context: any, ...params: infer P) => infer R ? [P, R] : never

type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void
? I extends Obj
? I
: never
: never

type AllState<Opt> = (Opt extends { state: infer S } ? Readonly<S extends () => infer P ? P : S> : undefined) &
(Opt extends { modules: infer SubM }
? Readonly<
{
[K in keyof SubM]: AllState<SubM[K]>
}
>
: unknown)

type GetMap<Opt, Target extends string, Pre = ''> = UnionToIntersection<
| (Opt extends { [_ in Target]: infer MM }
? {
[K in keyof MM as AddPrefix<K, Pre>]: GetParam<MM[K]>
}
: never)
| GetModulesMap<Opt, Target, Pre>
>

type GetModulesMap<Opt, Target extends string, Pre = ''> = Opt extends { modules: infer SubM }
? {
[K in keyof SubM]: GetMap<SubM[K], Target, AddNs<K, Pre, SubM[K]>>
}[keyof SubM]
: never

type AllGetter<Opt, G extends Obj = GetMap<Opt, 'getters'>> = {
readonly [K in keyof G]: G[K][1]
}
type AllCommit<Opt, G extends Obj = GetMap<Opt, 'mutations'>> = {
<K extends keyof G>(
type: K,
...payload: [...a: G[K][0] extends [] ? [payload?: null] : G[K][0], options?: CommitOptions]
): void
// <S extends { type: keyof G }>(payloadWithType: S, options?: CommitOptions): void
}
type AllDispatch<Opt, G extends Obj = GetMap<Opt, 'actions'>> = {
<S extends keyof G>(type: S, ...payload: G[S][0]): Promise<G[S][1]>
}

export interface Dispatch {
(type: string, payload?: any, options?: DispatchOptions): Promise<any>;
Expand Down