ざっくりプロジェクト情報
TypeScript
- version: 3.5.3
主なライブラリ
- React
- React-Router
- StyledComponent
- NestJs
- TypeORM
- class-validator
- class-transformer
tsconfig.json
Before:
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"noFallthroughCasesInSwitch": true,
"noImplicitAny": false,
"noImplicitReturns": true,
"noImplicitThis": false,
"noImplicitUseStrict": true,
"noUnusedLocals": true,
"noUnusedParameters": false,
"pretty": true,
"sourceMap": true,
"inlineSources": true,
"strictNullChecks": true,
"resolveJsonModule": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
After:
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"strict": true,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"pretty": true,
"sourceMap": true,
"inlineSources": true,
"resolveJsonModule": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
有効になるもの:
noImplicitAny
noImplicitThis
strictBindCallApply
strictFunctionTypes
strictPropertyInitialization
noUnusedParameters
(タイトルに反して strict
の他に noUnusedParameters
もtrueにしてます)
※基本的にPR、修正量が巨大になるので、一気に strict:true
にするのではなく、個々の設定値で分けて段階的に厳しくしていくこともおすすめです
直後の総エラー数
約1,000件
かなり少ない方、という感覚
ここから以下、出てきたエラーとその対応
noUnusedParameters
'変数名' is declared but its value is never read.
エラー数は大量だけど修正の仕方はあまり悩まないケースが多い
基本形(本当に不要)
対応:削除する。以上
削除できない / したくない
対応: _
プレフィックスをつけて利用しないことを明示する
// resにアクセスするけどreqは使わない
router.get("/", (req, res) => { })
↓
router.get("/", (_req, res) => { })
strictPropertyInitialization
Property 'プロパティ名' has no initializer and is not definitely assigned in the constructor.
基本形(本当に型が間違っている)
本当に初期化されないので undefined
になるのに、型がそうなっていない
対応:正しくnullableにする
class A {
hoge: string;
}
↓
class A {
hoge: string | undefined;
}
初期化していなくて実はundefinedで動いていたが実質それで良い
boolean型で初期値をとしてfalseを期待しているときなど、初期化してなくてもほぼ健在化しない
対応:初期値をセット
class A {
hoge: boolean;
}
class A {
hoge: boolean = false
}
良くない対応:必要ないのに、エラーを消すためにnullableにしてしまう
class A {
hoge: boolean | undefined
}
class-transfomerクラス
インスタンスの生成はclass-transfomerで行うので、コンストラクタは書きたくない
対応: !
をつけてしまう。
class A {
@Expose() readonly hoge: string;
}
↓
class A {
@Expose() readonly hoge!: string;
}
(TODO: privateのダミーコンストラクタを定義して、意図しないnewでの生成が出来ないようにできる気がするので、検証して追記)
TypeORMのEntityクラスで | null
が足りなかった
対応: 型や初期値を修正しつつ、このとき DB側の型が自動で識別されなくなるのでColumn()のパラメータも修正が必要
@Column({
nullable: true
})
@Type(() => Date)
acceptedAt: Date;
@Column("datetime", {
nullable: true
})
@Type(() => Date)
acceptedAt: Date | null = null;
プリミティブな型でないと、TypeORMが自動で識別できない( https://github.com/typeorm/typeorm/issues/1358 )
TypeORMのEntityクラスで、DBがセットするフィールドのno initializerエラー
auto incrementやcurrent timestampのフィールドは、初期化しようがない
対応: !
をつけてしまう
@PrimaryGeneratedColumn()
id: number;
↓
@PrimaryGeneratedColumn()
id!: number;
別解1: | null
を付ける
別解2: 未保存時用に別のinterfaceを定義する
このプロジェクトでは new
でエンティティを初期化して直後save
するケースがほとんどなので、 !
許容と判断しました
noImplicitAny
ImplicitAnyIndexErrors 以外
Parameter 'request' implicitly has an 'any' type.
基本形(単純に型アノテーションのつけわすれ)
対応:アノテーション付ける。以上
function func(hoge) {
}
function func(hoge: string) {
}
any型のオブジェクトに .map()
などを呼び出したとき
対応:ゆるい型でもよいのでanyを回避する
// resultsはany型
const results = await connection.query(query, [...]);
// r が怒られる
const convertedResults = results.map(r => {
});
const results: { [key: string]: unknown}[] = await connection.query(query, [...]);
const convertedResults = results.map(r => {
});
型定義がインストールされていない
基本形(単純にインストールし忘れ)
対応:インストールする。以上
`TS7016: Could not find a declaration file for module '@storybook/addon-actions'. 'パス〜/index.js' implicitly has an 'any' type.
yarn add -D @types/storybook__addon-actions
2つくらいインストールが漏れて暗黙的にanyで動いていた…
型定義が提供されていないライブラリ
対応:握りつぶしてしまう
import * as xlsxPopulate from "xlsx-populate";
// @ts-ignore: Could not find a declaration file for module
import * as xlsxPopulate from "xlsx-populate";
別解:自分で型情報を定義する
このプロジェクトのケースでは、そのライブラリを呼び出し箇所がごく少なく、複雑でもないので握りつぶしてOKと判断しました
anyのままは危険過ぎる場合や、モチベーションが湧く場合はもちろん型定義を書く判断はもちろんアリですが、
strict:true に変更してそのエラーを修正するブランチ・PRとは分けて対応したほうが良いと思います
最新の型ファイルだと使えない
query-string
で遭遇。
❯ yarn add -D @types/query-string
warning @types/query-string@6.3.0: This is a stub types definition. query-string provides its own type definitions, so you do not need this installed.
最新バージョンでは、ライブラリ本体に型定義が含まれるようになっていて、 @types/query-string
はダミーになっている。なのでインストールしても意味がない
そのうえで
This module targets Node.js 6 or later and the latest version of Chrome, Firefox, and Safari. If you want support for older browsers, or, if your project is using create-react-app v1, use version 5: npm install query-string@5.
old browsersサポートのためプロジェクトで5.xを使っている場合、ライブラリ本体にもインストールした @types/query-string
にも型情報が無いという状態になる
対応: typesもバージョンを指定してインストール
yarn add -D @types/query-string@5
ImplicitAnyIndexErrors
ブラケット記法でアクセスしているときに遭遇するエラー
基本形
対応:index typeを書く
const hash = {
a: 1,
b: 2,
c: 3
};
hash[key]
↓
const hash: { [key: string]: number } = {
a: 1,
b: 2,
c: 3
};
{}
型のimplicit any index errors
対応: 初期値が {}
なときは型を明示
// results: { key: string; count: number }[]
const resultMap = {};
for (const r of results) {
resultMap[r.key] = r.count;
}
const resultMap: { [key: string]: number } = {};
for (const r of results) {
resultMap[r.time] = r.count; // TS7053: Element implicitly has an 'any' type because expression of type 'any' can't be used to index type '{}'.
}
ジェネリクス
対応: index typeを継承していることを明示する
error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'unknown'.
No index signature with a parameter of type 'string' was found on type 'unknown'.
32 const value = obj[key];
func<T>(key, obj: T): T {
const value = obj[key];
}
↓
func<T extends { [key: string]: unknown >(key, obj: T): T {
const value = obj[key];
}
indexアクセスを回避できるとき
例その1
対応: 配列にできそう
const names = {
1: "山田 太郎",
2: "鈴木 一郎"
};
for (const i of Object.keys(names)) {
console.log(`[User${i}] name: ${names[i]}`);
}
const names = [
"山田 太郎",
"鈴木 一郎"
];
for (const [i, name] of names.entries()) {
console.log(`[User${i + 1}] name: ${name}`);
}
例その2
対応: Object.values()
や Object.entries()
が使えそう
const clientErrors = Object.keys(ClientErrorCode).map(
k => ClientErrorCode[k]
);
const clientErrors = Object.values(ClientErrorCode);
握りつぶす
ライブラリの型定義で手が出しづらいときなど
ブラケット記法で変数アクセスしているときは頑張っても型で安全性を高めるのが難しいことが多い気がするので、諦めも必要と思う
具体例: class-validatorでカスタムバリデーションを作成
https://github.com/typestack/class-validator#custom-validation-decorators
export function IsGreaterThan(
property: string,
validationOptions?: ValidationOptions
) {
return (object: Object, propertyName: string) => {
registerDecorator({
name: "IsGreaterThan",
target: object.constructor,
propertyName: propertyName,
constraints: [property],
options: validationOptions,
validator: {
validate(value: any, args: ValidationArguments) {
const [relatedPropertyName] = args.constraints;
const relatedValue = args.object[relatedPropertyName]; // Element implicitly has an 'any' type because expression of type 'any' can't be used to index type 'Object'.
return value > relatedValue;
}
}
});
};
}
↓
const relatedValue = (<any>args.object)[relatedPropertyName];
strictFunctionTypes
基本形(本当に型が一致していない)
例: Uint8Array と number[]
対応:適切に変換する
TS2345: Argument of type 'Uint8Array' is not assignable to parameter of type 'number[]'.
Type 'Uint8Array' is missing the following properties from type 'number[]': pop, push, concat, shift, and 3 more.
String.fromCharCode.apply(
null,
new Uint8Array(data.slice(start, end))
);
↓
String.fromCharCode.apply(
null,
Array.from(new Uint8Array(data.slice(start, end)))
);
nullable引数の罠
strictFunctionTypesを有効にすると、「関数引数の引数」のNullable/NonNullableの条件は混乱しがち(「普通の変数」のassignのときと逆転する)
Reactのイベントハンドラー周りで遭遇しがちと思う
TS2322: Type '(_e: MouseEvent<HTMLElement, MouseEvent>) => Promise<void>' is not assignable to type 'event?: MouseEvent<HTMLElement, MouseEvent>'.
Types of parameters '_e' and 'event' are incompatible.
Type 'MouseEvent<HTMLElement, MouseEvent> | undefined' is not assignable to type 'MouseEvent<HTMLElement, MouseEvent>'.
Type 'undefined' is not assignable to type 'MouseEvent<HTMLElement, MouseEvent>'.
public render() {
// Componentの props.handler: (event?: MouseEvent<HTMLElement, MouseEvent>)
return <Component handler={this.handler} />;
}
private handler = (e: MouseEvent<HTMLElement, MouseEvent>) => {
};
対応: MouseEvent
の省略可・不可 (e:
か e?:
か)を正しく合わせる
↓
private handler = (e?: MouseEvent<HTMLElement, MouseEvent>) => {
};
withRouter() のときの正しいprops
TS2345: Argument of type 'typeof Component' is not assignable to parameter of type 'ComponentType<RouteComponentProps<any, StaticContext, any>>'.
Type 'typeof Component' is not assignable to type 'ComponentClass<RouteComponentProps<any, StaticContext, any>, any>'.
Type 'Component' is not assignable to type 'Component<RouteComponentProps<any, StaticContext, any>, any, any>'.
Types of property 'props' are incompatible.
Type 'Readonly<IProps> & Readonly<{ children?: ReactNode; }>' is not assignable to type 'Readonly<RouteComponentProps<any, StaticContext, any>> & Readonly<{ children?: ReactNode; }>'.
Property 'location' is missing in type 'Readonly<IProps> & Readonly<{ children?: ReactNode; }>' but required in type 'Readonly<RouteComponentProps<any, StaticContext, any>>'.
import { withRouter } from "react-router";
type Props = {
history: History;
};
class Component extends React.Component<Props, State> {
}
export default withRouter(Component);
対応: RouteComponentProps
が用意してあるので使う
↓
import { RouteComponentProps, withRouter } from "react-router";
type Props = RouteComponentProps;
さいごに
null assertionやts-ignoreなどを完全に排除するよう頑張るよりも、多少は受け入れるくらいの柔軟さは有ってよいのでは派です
noImplicitAny: false
のままにしたり、 noImplicitAny: true
でも suppressImplicitAnyIndexErrors: true
にするなど、無理せず柔軟に、チームが快適な型安全性レベルを選べばよいと思います
宣伝
この記事の内容は、株式会社EventHubのプロジェクトでの実例に基づきます。
株式会社EventHub では「イベントでの繋がりを増やす」をテーマに、参加者同士の情報交換・面談を促進するイベント・マーケティングツールを開発しています。
エンジニア積極採用中ですのでご興味がある方は support@eventhub.jp まで連絡お願いします。