23
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

TypeScriptAdvent Calendar 2020

Day 18

`let h = null`について、TypeScript はどのような型を推論するか

Posted at

はじめに

Q. let h = nullについて、TypeScriptはどのような型を推論するでしょうか? (▶ をクリックすると回答が表示されます) A. any 型 (`let h: any`) となる[^1] [^1]: `tsconfig.json`において`strictNullChecks`が`true`かつ、`noImplicitAny`が`false`となっている場合は null 型になります。

h=null.PNG



こちらの問いは、プログラミング TypeScript ――スケールする JavaScript アプリケーション開発に記載されていた問いになります。

皆さんは正解しましたか?

(ちなみに、const h = nullとした場合、hは null 型となります)

本記事では、なぜこのような結果になるのか解説したいと思います。

前提

  • バージョン

    $ tsc -v
    Version 4.1.3
    
  • tsconfig.jsonにおいて"strictNullChecks": trueかつ、"noImplicitAny": true1

tsconfig.json
{
  "compilerOptions": {
    "strictNullChecks": true,
    "noImplicitAny": true,
    // or
    "strict": true, // strictがtrueだと、上記のオプション含めた複数のオプションがtrueになります
  }
}

解説

結論

null 型、undefined 型を、 letvarといった後で変更可能な方法で宣言した場合、any 型に拡張されます。
これは型の拡大(type widening)と呼ばれます2
null や undefined で型の拡大を利用するパターンは稀ですが、リテラル型からプリミティブ型への拡張はよく利用されるかと思います。
例えば以下のように宣言した場合、リテラル型である1constで宣言した場合はそのまま1型になり、letで宣言した場合はプリミティブ型であるnumber型へ拡張されます。

const one = 1; // 'one' has type: 1
let num = 1; // 'num' has type: number

引用: TypeScript-New-Handbook/Widening-and-Narrowing


以下、詳しく説明します。

プリミティブ型とリテラル型

型の拡大に触れる前に、TypeScript で扱う型について整理しておきます。
(TypeScript で扱う型についてはTypeScript の型入門が詳しいです)

簡単に説明しますと、

  • プリミティブ型は、string, number, bigint, boolean, undefined,symbolの 6 種類を指します3
  • リテラル型は、string, number, boolean型の値そのものを指す型であり、それ以外の値を受け入れることができません
const one = 1; // one は 1型 というリテラル型
let foo: "foo" = "foo";
foo = "bar"; // ERROR: Type '"bar"' is not assignable to type '"foo"'.ts(2322)

型の拡大(type widening)

型の拡大により、変数を変更可能な方法で宣言したときに、以下のように型が変化します。

  1. null 型、undefined 型を any 型として扱う
    • strictNullCheckstrue/falseによって挙動が異なる
  2. リテラル型をプリミティブ型として扱う
  3. Enum 型のメンバは、それを含む Enum 型として扱う

変数を変更可能な方法とは、varletでの変数宣言だけでなく、オブジェクトや配列の宣言も含みます(知っての通り、JavaScript のこれらは変更可能なので)。

1. null 型、undefined 型を any 型として扱う

strict: trueの場合、letvarで宣言した null 型や undefined 型は any に拡大されます。
しかし、オブジェクトや配列の要素として宣言した null 型は拡大されません。

// strictNullChecks: true, noImplicitAny: true
// or strict: true
let h = null; // any type
const i = null; // null type
let j = undefined; // any type
const k = undefined; // undefined type

const l = {
  m: null,
  n: undefined,
};
// const l = {
//   m: null,
//   n: undefined,
// };

const o = [null, undefined];
// const o: (null | undefined)[]

strictNullChecks: truenoImplicitAny: falseの場合、letvarで宣言した null 型や undefined 型も拡大されず、 null 型、undefined 型のままになります4

// strictNullChecks: true, noImplicitAny: false
let h = null; // null type
const i = null; // null type
let j = undefined; // undefined type
const k = undefined; // undefined type
// object, arrayの挙動は上と同様

strictNullChecks: falseの場合、null 型、undefined 型は明示的に宣言しない限り any 型へと拡大されます。

// strictNullChecks: false
let h = null; // any type
const i = null; // any type
let j = undefined; // any type
const k = undefined; // any type

const l = {
  m: null,
  n: undefined,
};
// const l = {
//   m: any,
//   n: any,
// };

const o = [null, undefined];
// const o: any[]

let p: null = null; // null type
const q: undefined = undefined; // undefined type

ただし、null 型、undefined 型で宣言された変数がそのスコープを離れると、明確な型が割り当てられます。

function x() {
  // function x(): null
  let a = null; // let a: any
  return a;
}

function y() {
  // function y(): string
  let a = null; // let a: any
  a = 3; // let a: any
  a = "b"; // let a: any
  return a;
}
let z = y(); // let z: string

2. リテラル型をプリミティブ型として扱う

変数を変更可能な方法で宣言したときに、以下のように拡大されます。

const h = 1; // 1 type (literal type)
let i = 1; // number type (primitive type)

// letの場合も同様
const j = {
  k: "k",
  l: 0,
  m: true,
};
// const j: {
//     k: string;
//     l: number;
//     m: boolean;
// }

const n = ["n", 0, false];
// const n: (string | number | boolean)[]

ただし、2 つの注意点があります。

  1. リテラル型の拡大は、式によるリテラル型にのみ発生し、型によるリテラル型では発生しない
  2. リテラル型の拡大は、宣言されたリテラル型が変更可能な場所に到達するたびに発生する

式によって宣言されたリテラル型TypeScript-New-Handbook/Widening-and-Narrowingでは、fresh literal typeと呼ばれ、リテラル型とは区別されています。

const o = 1; // `1`という式によってリテラル型を宣言 (= fresh literal type)
const p: 1 = 1; // `: 1`という型によってリテラル型を宣言 (= literal type)
const q = {
  o: o,
  p: p,
};
// オブジェクトのプロパティや配列は変更可能なので、
// 式で宣言されたリテラル型はnumber型に拡大される
// ただし、型で宣言されたリテラル型は拡大されない
// const q: {
//     o: number;
//     p: 1;
// }
const r = [o]; // const r: number[]
const s = [p]; // const r: 1[]

// もちろん、letやvarで宣言した場合も、上記と同様に拡大される
let t = o; // let t: number
let u = p; // let u: 1

3. Enum 型のメンバは、それを含む Enum 型として扱う

enum A {
  B,
  C,
}

const b = A.B; // const b: A.B
let c = A.C; // let c: A
let d = b; // let d: A

型を拡大したくない場合

const アサーションを用いて、型アサーション5を行うことで、型の拡大を抑えることが出来ます。
また、オブジェクトや配列に対して const アサーションを用いると、再帰的にReadOnlyに指定します。

let a = 1; // let a: number
let b = 1 as const; // let b: 1

let c = { x: 1 }; // let c: { x: number; }
let d: { x: 1 } = { x: 1 }; // let d: { x: 1; }
let e = { x: 1 } as const; // let e: { readonly x: 1; }

最後に

上の挙動は、TypeScript に触れていれば感覚で理解できていると思います。
しかし、挙動を明確に把握することで、予期せぬ型推論を防ぐことができると思います。
この記事が皆さんのお役に立てれば幸いです。

参考文献

  1. "strictNullChecks": falseの場合、constで定義した null も、any 型になります。
    また、"strictNullChecks": trueかつ"noImplicitAny": falseの場合、letで定義した null はconstと同様に null 型になります。
    各オプションについては以下を参照:

  2. プログラミング TypeScript ――スケールする JavaScript アプリケーション開発の中でtype widening型の拡大と訳していたため、それに則っています。

  3. 参照: MDN Web Docs 用語集: ウェブ関連用語の定義 | Primitive (プリミティブ)
    ここでは null 型をプリミティブ型に含んでいませんが、記事によっては含んでいるものもあります。

  4. どうしてこのような挙動の違いがあるのかまで、調べきれてません。ご存知の方いらっしゃいましたらご教示いただけますと幸いです。

  5. 型アサーションとは、TypeScript が推論した型に対して、型の上書きを行う方法です。

23
7
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
23
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?