はじめに
TypeScript には間違った使い方をすると型の恩恵が受けられなくなるものの、正しく使えば便利な機能がいくつかあります。
また特に Vue においては比較的 as
などを使う機会が多く心理的ハードルが下がり間違ったケースでも使われてしまうケースが多いように感じます。
そこでどういう使い方が間違っていて、どう使うのが正しいのかをまとめ、正しく TypeScript の機能を使いましょうということでこの記事を書きました。
TypeScript の気をつけたい機能
any
いわずもがな。
あらゆるデータを受け付ける型で、乱用すると TypeScript を使っている意味がなくなってしまうため any
であることが正しいと説明できる場合以外は できる限り利用は避けましょう。
as
: 型アサーション、 Type Assertion
as
は正しく型推論できない場合などに 型をコンパイラに教える ための機能です。
ただし 値が変換されるわけではない ため間違った型を設定すると、実際のデータと型が一致しなくなり 実行時エラーとなる場合がある ので気をつけましょう。
✗ Bad
user
は User
型であると as
を使って コンパイラに教えているため user.profile.gender
はコンパイルエラーになりませんが、実際は profile
プロパティを持っていないため実行時エラーとなります。
type User = {
id: number;
name: string;
profile: {
gender: string;
}
}
const user = {
id: 1,
name: "yamagen0915",
} as User;
console.log(user.profile.gender);
// Uncaught TypeError: Cannot read property 'gender' of undefined
○ Good
どう対処すべきかはケースバイケースですが、
// 型定義に合わせ profile を設定する
const user: User = {
id: 1,
name: "yamagen0915",
profile: {
gender: 'male'
}
}
!.
: Non Null Assertion
Nullable でないことをコンパイラに教える 機能です。
(Nullable = null
or undefined
)
as
と同様 Nullable になり得る場所で使ってしまうと 実行時エラーとなる場合がある ので気をつけましょう。
✗ Bad
profile?
へ !.
でアクセスしているためコンパイルエラーになりませんが、実際は profile
プロパティを持っていないため実行時エラーとなります。
type User = {
id: number;
name: string;
profile?: {
gender: string;
}
}
const user: User = {
id: 1,
name: "yamagen0915",
};
console.log(user.profile!.gender);
// Uncaught TypeError: Cannot read property 'gender' of undefined
○ Good
null チェックを行うようにしましょう。
if (user.profile) {
console.log(user.profile.gender);
}
Vue の具体例
Vue2, Vue3 Composition API において具体的にどのようにそれらの機能を使うか、または使わないべきかを紹介していきます。
methods や関数の引数
関数の引数は型を明示する必要 があります。
✗ Bad
引数に型を明示しないと any
となります。
export default Vue.extend({
methods: {
// id: any, name: any
foo(id, name) {}
}
});
// id: any, name: any
const foo = (id, name) => {}
○ Good
export default Vue.extend({
methods: {
foo(id: number, name: string) {}
}
});
const foo = (id: number, name: string) {}
○ Good
関数の引数に渡す関数などはその引数の定義から型推論されます。
// n: number
[1, 2, 3, 4, 5].map(n => n + 1);
○ Good
デフォルト値が存在する場合はデフォルト値の型から型推論されます。
export default Vue.extend({
methods: {
// id: number, name: string
foo(id = 0, name = '') {}
}
});
// id: number, name: string
const foo = (id = 0, name = '') => {}
template ref
template ref では as を利用しコンパイラに型を教える必要 があります。
✗ Bad
// <input ref="input">
// input: Vue | Vue[] | Element | Element[]
const input = this.$refs.input
// <custom-component ref="custom"/>
// custom: Vue | Vue[] | Element | Element[]
const custom = this.$refs.custom
○ Good
取得する値に応じて適切な型を as
を使って設定しましょう。
ただし $refs
で取得できるデータに変更があってもコンパイルエラーにはならないため気をつけましょう。
ちなみに input <-> HTMLInputElement のような対応は下記の定義通りです。
// <input ref="input">
// input: HTMLInputElement
const input = this.$refs.input as HTMLInputElement
// <custom-component ref="custom"/>
// custom: CustomComponent
const custom = this.$refs.custom as InstanceType<typeof CustomComponent>
Composition API template ref
Coposition API では ref を使って template ref と連携しますが、その ref の型引数に型を指定することができます。
ただしその変数の 宣言時には値を入れることができないため Nullable になります。
都度 null チェックを行うのは面倒なため Non Null Assertion の利用を検討しましょう。
○ Good
Non Null Assertion を利用せず都度 null チェックを行ってももちろん問題ありません。
setup() {
// input: HTMLInputElement | undefined
const input = ref<HTMLInputElement>();
// custom: CustomComponent | undefined
const custom = ref<InstanceType<typeof CustomComponent>>();
// 例えばボタンクリックで input, custom へアクセスする場合
const click = () => {
if (input.value) {
console.log(input.value.value);
}
if (custom.value) {
console.log(custom.value.foo);
}
};
return {
input,
custom,
click,
};
}
○ Good
template ref の設定が適切な前提で Non Null Assertion を利用することも検討しましょう。
個人的には template ref の設定が間違っている場合に null チェックを行うと実行時エラーになりませんが、エラーになったほうがよいと思うのでこちらの書き方のほうがよいと思っています。
setup() {
// input: HTMLInputElement | undefined
const input = ref<HTMLInputElement>();
// custom: CustomComponent | undefined
const custom = ref<InstanceType<typeof CustomComponent>>();
// 例えばボタンクリックで input, custom へアクセスする場合
const click = () => {
console.log(input.value!.value);
console.log(custom.value!.foo);
};
return {
input,
custom,
click,
};
}
data()
期待通りに型推論されないこともあるため 変数として切り出したり as
を使って型を明示する必要 があります。
最終的にオブジェクトの型が正しく型推論されれば良いので対処方法は様々です。
✗ Bad
[]
, undefined
, null
は期待通りに型推論されません。
data() {
return {
list: [], // any[]
id: undefined, // undefined
name: null, // null
};
}
○ Good
各プロパティを変数として切り出し型を明示したり、undefined
, null
の利用をさけるようにしましょう。
data() {
const list: string[] = [];
return {
list, // string[]
id: 0, // number
name: '', // string
};
}
○ Good
as
を使って型を明示しましょう。
また初期値として undefined
, null
を使う必要がある場合は | null
を忘れずにつけるようにしましょう。
data() {
return {
list: [] as string[], // string[]
id: undefined as number | undefined, // number | undefined
name: null as string | null, // string | null
};
}
✗ Bad
オブジェクトの型を as
で明示する場合、型の変更があった場合にエラーとならないため避けたほうが良い場合があります。
type User = {
id: number;
name: string;
};
data() {
return {
// 仮に gender: string が追加されてもエラーとならず型と値が一致しなくなってしまう。
user: {
id: 1,
name: "yamagen0915",
} as User,
};
}
○ Good
変数として切り出し型を明示することで型に変更があった場合にコンパイルエラーとすることができます。
type User = {
id: number;
name: string;
};
data() {
// 仮に gender: string が追加された場合にエラーとなる
const user: User = {
id: 1,
name: "yamagen0915",
};
return {
user,
};
}
axios
Vue, TypeScript と直接は関係ありませんが API へのリクエストを axios を利用しているプロジェクトも多いと思うので紹介します。
axios.get
等には レスポンスの型を型引数として指定することができる ため設定しましょう。
✗ Bad
型引数に何も指定しない場合 response.data
は any
となります。
const response = await axios.get('/api/users');
response.data // any
○ Good
型引数に API のレスポンスの型を指定します。
ただし API のレスポンスが本当にその型の通りかどうかは保証されないため API の仕様が変わった場合は合わせて型定義も合わせて修正するようにしましょう。
type User = {
id: number;
name: string;
};
const response = await axios.get<User[]>('/api/users');
response.data // User[]