LoginSignup
21
25

More than 3 years have passed since last update.

TypeScriptプロジェクトでstrict:trueに切り替えた時に出たエラーと対応まとめ

Last updated at Posted at 2019-07-12

ざっくりプロジェクト情報

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 まで連絡お願いします。

21
25
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
25