Help us understand the problem. What is going on with this article?

Vue Composition APIで型がぶっ壊れて楽しかったです

Advent Calnder.png

これは bosyu Advent Calendar 2019 の 4日目の記事です。
よろしくね。

bosyuのフロントはVueを使っているのですが、最近 Composition API が上がってきているのでそれを使うようにしていこうぜ〜〜〜
という感じでやっております。

https://github.com/vuejs/composition-api
これね。

でこれがたまにアレなところがあるのですが、先日おもしろいところを見つけたので、それについて書いていくぞというアレです。
多分近々直ります。

なにがおきた

タイトル通りなんですが、型が不思議になりました。
具体的なコードを書くと以下のようなときに型が?????となります。

nazo.vue
export default createComponent({
  setup () {
    const state = reactive({
      status: 'hoge',
      value: 'fuga'
    });
    const v = state.value;
  }
});

image.png

ここでは state は以下のようになるように期待していますが、

type State = {
  status: string;
  value: string;
}

VSCode等でみると statestring となってしまいます :innocent:

なんでや

ということで、どんなかんじに型定義されているかみてみましょ。

https://github.com/vuejs/composition-api/blob/v0.3.4/src/reactivity/reactive.ts#L129

export function reactive<T = any>(obj: T): UnwrapRef<T> {
  if (process.env.NODE_ENV !== 'production' && !obj) {
    warn('"reactive()" is called without provide an "object".');
    // @ts-ignore
    return;
  }

  if (!isPlainObject(obj) || isReactive(obj) || isNonReactive(obj) || !Object.isExtensible(obj)) {
    return obj as any;
  }

  const observed = observe(obj);
  def(observed, ReactiveIdentifierKey, ReactiveIdentifier);
  setupAccessControl(observed);
  return observed as UnwrapRef<T>;
}

こんなかんじでした。
return のところの型が as になってて強さを感じる。

まあそれはさておきこの UnwrapRef<T> てのがきになりますね。
それを見てみましょう。

https://github.com/vuejs/composition-api/blob/v0.3.4/src/reactivity/ref.ts#L17

export type UnwrapRef<T> = T extends Ref<infer V>
  ? UnwrapRef2<V>
  : T extends BailTypes
      ? T // bail out on types that shouldn’t be unwrapped
      : T extends object ? { [K in keyof T]: UnwrapRef2<T[K]> } : T

// prettier-ignore
type UnwrapRef2<T> = T extends Ref<infer V>
  ? UnwrapRef3<V>
  : T extends BailTypes
      ? T
      : T extends object ? { [K in keyof T]: UnwrapRef3<T[K]> } : T

// ...

// prettier-ignore
type UnwrapRef10<T> = T extends Ref<infer V>
  ? V // stop recursion
  : T 

こんな感じになってます。
ConditionalTypes をつかって型を判定していますね。
http://www.typescriptlang.org/docs/handbook/advanced-types.html#conditional-types

でこの Ref の定義を見てみましょう。

https://github.com/vuejs/composition-api/blob/v0.3.4/src/reactivity/ref.ts#L9

export interface Ref<T> {
  value: T;
}

です。

つまり value をメンバーとして持っている場合は Ref を extends しているとみなされてしまい、
UnwrapRef2 UnwrapRef3 ... と流れていってしまいます。
最終的に UnwrapRef10 にたどり着き V となります。

結果的に最初に書いたように statestring のようになってしまうわけですね〜。

かいけつさく

https://github.com/vuejs/composition-api/pull/167
PRはでてますが、 Object の中に Object があったりすると、同様の問題が起きてしまうので困ったもんだなぁ〜という感じですね。

かんたんにはどうこうできるような感じではないので、できれば value というのはできる限りつかわないようにするのがとりあえずの対策かなぁ〜。

またVue-nextでは同様の問題が起きないような対処がされているようです。
https://github.com/vuejs/vue-next/blob/master/packages/reactivity/src/ref.ts

なのでこれと同様な実装を composition-api にPRでなげるかとかですかね。

まあそんな感じ。

あとは邪悪だけど

createComponent({
  setup () {
    const state = reactive({
      status: 'hoge',
      value: 'fuga'
    }) as unknown as { status: string, value: string };
    const v = state.value;
  }
});

unknown とか any とか一回型をぶっ壊して、むりやりやっちゃうとかね。
最高にダメな感じだけど。

まとめ

  • かたはむずかしい。
  • かたはたのしい。
  • as unknow as Hoge は最凶(強)。
merotan
ふろんとえんどとかるびぃとかをやってる CSSは悪い文明! 粉砕する!
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away