Desmond

Desmond

An introvert who loves web programming, graphic design and guitar
github
bilibili
twitter

TypeScriptの型の基本

原始タイプ#

const name: string = 'linbudu';
const age: number = 24;
const male: boolean = false;
const undef: undefined = undefined;
const nul: null = null;
const obj: object = { name, age, male };
const bigintVar1: bigint = 9007199254740991n;
const bigintVar2: bigint = BigInt(9007199254740991);
const symbolVar: symbol = Symbol('unique'); 

null と undefined#

JavaScript において、null と undefined はそれぞれ「ここに値があるが空の値」と「ここに値がない」を表します。一方、TypeScript では、null と undefined の型は具体的な意味を持つ型です。つまり、これらは型として使用されるとき、意味のある具体的な型値を表します。strictNullChecks チェックが有効でない場合、これらは他の型のサブタイプと見なされます。例えば、string 型は null と undefined 型を含むと見なされます:

const tmp1: null = null;
const tmp2: undefined = undefined;

const tmp3: string = null; // strictNullChecks が無効な場合のみ成立、以下同様
const tmp4: string = undefined;

void#

TypeScript の原始型注釈には void もありますが、JavaScript とは異なり、ここでの void は内部に return 文がない、または明示的に値を返さない関数の戻り値を説明するために使用されます:

function func1() {}
function func2() {
  return;
}
function func3() {
  return undefined;
}

ここでは、func1 と func2 の戻り値の型は暗黙的に void と推論され、明示的に undefined 値を返す func3 のみがその戻り値の型を undefined と推論されます。しかし、実際のコード実行では、func1 と func2 の戻り値はどちらも undefined です。

func3 の戻り値の型は undefined と推論されますが、void 型で注釈を付けることもできます。なぜなら、型の観点から func1、func2、func3 はすべて「意味のある値を返さない」を表すからです。

ここは少しややこしいかもしれませんが、void は空の型を表し、null と undefined は意味のある実際の型であると考えることができます(JavaScript における意味との区別に注意してください)。undefined は void 型の変数に代入でき、JavaScript では戻り値のない関数がデフォルトで undefined を返すようにします。null 型も同様ですが、strictNullChecks 設定を無効にする必要があります。

const voidVar1: void = undefined;
const voidVar2: void = null; // strictNullChecks を無効にする必要があります

配列型#

配列もまた、TypeScript で最も一般的に使用される型の一つです。配列型を宣言するには、TypeScript では二つの方法があります:

const arr1: string[] = [];
const arr2: Array<string> = [];

この二つの方法は完全に同等ですが、実際には前者が主に使用されます。arr2 にマウスをホバーすると、表示される型の署名は string[] であることがわかります。配列は日常の開発で大量に使用されるデータ構造ですが、特定の状況では、タプル(Tuple) を使用して配列の代わりにする方が適切です。例えば、固定長の変数のみを格納する配列で、境界を越えたアクセス時に型エラーを出したい場合です。

const arr4: [string, string, string] = ['lin', 'bu', 'du'];

console.log(arr4[599]);

この場合、型エラーが発生します:長さ「3」のタプル型「[string, string, string]」はインデックス「599」で要素を持ちません。同じ型の要素以外にも、タプル内部で位置に強くバインドされた異なる型の要素を宣言することもできます:

const arr5: [string, number, boolean] = ['linbudu', 599, true];

この場合、配列の合法的な境界内のインデックスアクセス(つまり 0、1、2)は、対応する位置の型を正確に取得します。同時に、タプルは特定の位置にオプションのメンバーを持つこともサポートしています:

const arr6: [string, number?, boolean?] = ['linbudu'];
// 以下のように書くこともできます
// const arr6: [string, number?, boolean?] = ['linbudu', , ,];

オプションとしてマークされたメンバーは、--strictNullCheckes 設定下で string | undefined 型と見なされます。この時、タプルの長さのプロパティも変化します。例えば、上記のタプル arr6 の長さの型は 1 | 2 | 3 です:

type TupleLength = typeof arr6.length; // 1 | 2 | 3

タプルの可読性は実際にはあまり良くないと感じるかもしれません。例えば、[string, number, boolean] の場合、これらの三つの要素が何を表しているのか直接知ることはできず、オブジェクト形式を使用する方が良いかもしれません。しかし、TypeScript 4.0 では、名前付きタプル(Labeled Tuple Elements)のサポートにより、タプル内の要素に属性のようなマークを付けることができるようになりました:

const arr7: [name: string, age: number, male?: boolean] = ['linbudu', 599, true];

タプルに対する暗黙の境界を越えたアクセスも警告を出すことができます:

const arr5: [string, number, boolean] = ['linbudu', 599, true];

// 長さ「3」のタプル型「[string, number, boolean]」はインデックス「3」で要素を持ちません。
const [name, age, male, other] = arr5;

オブジェクト型#

  • 各プロパティの値はインターフェースのプロパティ型に一対一で対応しなければなりません。
  • 余分なプロパティを持つことはできず、また少ないプロパティを持つこともできません。これには、オブジェクト内部での宣言や obj1.other = 'xxx' のようなプロパティアクセスによる代入も含まれます。

オプション(Optional)& 読み取り専用(Readonly)#

interface IDescription {
  name: string;
  age: number;
  male?: boolean;
  func?: Function;
}

const obj2: IDescription = {
  name: 'linbudu',
  age: 599,
  male: true,
  // func を実装する必要はありません
};
interface IDescription {
  readonly name: string;
  age: number;
}

const obj3: IDescription = {
  name: 'linbudu',
  age: 599,
};

// "name" に割り当てることはできません。なぜならそれは読み取り専用プロパティだからです。
obj3.name = "林不渡";

実際、配列とタプルのレベルでも読み取り専用の修飾がありますが、オブジェクト型とは二つの点で異なります。

  • 配列 / タプル全体を読み取り専用としてマークすることしかできず、オブジェクトのように特定のプロパティを読み取り専用としてマークすることはできません。
  • 一度読み取り専用としてマークされると、その読み取り専用配列 / タプルの型は、push、pop などのメソッド(つまり元の配列を変更するメソッド)を持たなくなります。したがって、エラーメッセージは型 xxx に「push」プロパティは存在しませんとなります。この実装の本質は、読み取り専用配列と読み取り専用タプルの型は実際には ReadonlyArray に変わり、Array ではなくなるということです。

type と interface#

interface はオブジェクトやクラスの構造を記述するために使用され、型エイリアス type は関数のシグネチャ、一連のユニオン型、ツール型などを抽出して独立した型を作成するために使用されます。

object、Object および { }#

Object はすべての型を含みます:

// undefined、null、void 0 に対しては strictNullChecks を無効にする必要があります
const tmp1: Object = undefined;
const tmp2: Object = null;
const tmp3: Object = void 0;

const tmp4: Object = 'linbudu';
const tmp5: Object = 599;
const tmp6: Object = { name: 'linbudu' };
const tmp7: Object = () => {};
const tmp8: Object = [];

Object に似た Boolean、Number、String、Symbol といういくつかのボックス型(Boxed Types)も同様に、予期しない型を含むことがあります。String の例を挙げると、undefined、null、void、そして代表するアンボックス型(Unboxed Types) string を含みますが、他のボックス型に対応するアンボックス型(boolean および基本オブジェクト型)は含まれません。以下のコードを見てみましょう:

const tmp9: String = undefined;
const tmp10: String = null;
const tmp11: String = void 0;
const tmp12: String = 'linbudu';

// 以下は成立しません。なぜなら文字列型のアンボックス型ではないからです。
const tmp13: String = 599; // X
const tmp14: String = { name: 'linbudu' }; // X
const tmp15: String = () => {}; // X
const tmp16: String = []; // X

どんな場合でも、これらのボックス型を使用すべきではありません。

object の導入は Object 型の誤った使用を解決するためのもので、すべての非原始型の型、つまり配列、オブジェクト、関数型などを表します

const tmp17: object = undefined;
const tmp18: object = null;
const tmp19: object = void 0;

const tmp20: object = 'linbudu';  // X 成立しません。値は原始型です。
const tmp21: object = 599; // X 成立しません。値は原始型です。

const tmp22: object = { name: 'linbudu' };
const tmp23: object = () => {};
const tmp24: object = [];

最後に {} ですが、これは奇妙な空のオブジェクトです。リテラル型について知っている場合、{} はオブジェクトリテラル型(文字列リテラル型に対応)であると考えることができます。そうでない場合、{} を型署名として使用することは合法ですが、内部にプロパティ定義がない空のオブジェクトであると考えることができます。これは Object(new Object() を考えてみてください)に似ており、非 null /undefined の値を意味します:

const tmp25: {} = undefined; // strictNullChecks を無効にする必要があります、以下同様
const tmp26: {} = null;
const tmp27: {} = void 0; // void 0 は undefined と等価です

const tmp28: {} = 'linbudu';
const tmp29: {} = 599;
const tmp30: {} = { name: 'linbudu' };
const tmp31: {} = () => {};
const tmp32: {} = [];

この変数を型として使用することはできますが、実際にはこの変数に対して何らかの代入操作を行うことはできません

const tmp30: {} = { name: 'linbudu' };
tmp30.age = 18; // X 型「{}」上に「age」プロパティは存在しません。

私たちもまた {} の使用を避けるべきです。{} は非 null /undefined の値を意味します。この観点から見ると、これを使用することは any を使用するのと同じくらい悪いです。

ある変数の具体的な型が不明であるが、原始型ではないことが確実な場合は object を使用できます。しかし、さらに区別することをお勧めします。つまり、Record<string, unknown> または Record<string, any> をオブジェクトを表すために、unknown[] または any[] を配列を表すために、(...args: any[]) => any を関数を表すために使用することです。

リテラル型(Literal Types)#

リテラル型は原始型よりも正確な型を表し、原始型のサブタイプでもあります。

リテラル型には主に文字列リテラル型数値リテラル型ブールリテラル型、およびオブジェクトリテラル型が含まれ、これらは型注釈として直接使用できます:

const str: "linbudu" = "linbudu";
const num: 599 = 599;
const bool: true = true;

// エラー!型「"linbudu599"」を型「"linbudu"」に割り当てることはできません。
const str1: "linbudu" = "linbudu599";

const str2: string = "linbudu";
const str3: string = "linbudu599";

ユニオン型#

ユニオン型は、一組の型の利用可能な集合を表すと理解できます。最終的に割り当てられる型がユニオン型のメンバーの一つに属していれば、このユニオン型に適合すると考えられます。

interface Tmp {
  bool: true | false;
  num: 1 | 2 | 3;
  str: "lin" | "bu" | "du"
  mixed: true | string | 599 | {} | (() => {}) | (1 | 2)
}
  • ユニオン型内の関数型は、括弧 () で囲む必要があります。
  • 関数型にはリテラル型は存在しないため、ここでの (() => {}) は合法的な関数型です。
  • ユニオン型内でさらにユニオン型をネストすることもできますが、これらのネストされたユニオン型は最終的に第一階層に平坦化されます。

ユニオン型の一般的な使用シーンの一つは、複数のオブジェクト型のユニオンを通じて手動の排他的プロパティを実現することです。つまり、このプロパティにフィールド 1 がある場合、フィールド 2 は存在しないということです:

interface Tmp {
  user:
    | {
        vip: true;
        expires: string;
      }
    | {
        vip: false;
        promotion: string;
      };
}

declare var tmp: Tmp;

if (tmp.user.vip) {
  console.log(tmp.user.expires);
}

この例では、user プロパティは通常のユーザーと VIP ユーザーの二つの型を満たします。ここで vip プロパティの型はブールリテラル型で宣言されています。実際の使用時には、このプロパティが true であることを確認することで、次の型推論が VIP ユーザーの型(つまりユニオン型の最初の分岐)に絞られることを保証できます。

また、型エイリアスを使用して一連のリテラルユニオン型を再利用することもできます:

type Code = 10000 | 10001 | 50000;
type Status = "success" | "failure";

列挙型#

enum PageUrl {
  Home_Page_Url = "url1",
  Setting_Page_Url = "url2",
  Share_Page_Url = "url3",
}

const home = PageUrl.Home_Page_Url;

このようにすることで、利点は非常に明確です。まず、より良い型ヒントを得ることができます。次に、これらの定数は実際に名前空間内に制約されます(上記のオブジェクト宣言は常に少し足りません)。列挙型の値を指定しなかった場合、デフォルトで数字の列挙型が使用され、0 から始まり、1 ずつ増加します。

特定のメンバーにのみ列挙値を指定した場合、以前に値が割り当てられていないメンバーは、0 から増加する方式を使用し、その後のメンバーは列挙値から増加を開始します。

enum Items {
  // 0 
  Foo,
  Bar = 599,
  // 600
  Baz
}

数値型の列挙型では、遅延評価の列挙値を使用することができます。例えば関数:

const returnNum = () => 100 + 499;

enum Items {
  Foo = returnNum(),
  Bar = 599,
  Baz
}

ただし、遅延評価の列挙値には条件があります。遅延評価を使用する場合、遅延評価を使用していない列挙メンバーは、遅延評価の列挙値を宣言したメンバーの後に配置する必要があります(上記の例のように)、または最初に配置する必要があります

enum Items {
  Baz,
  Foo = returnNum(),
  Bar = 599,
}

TypeScript では、文字列の列挙値と数値の列挙値を同時に使用することもできます:

enum Mixed {
  Num = 599,
  Str = "linbudu"
}

列挙型とオブジェクトの重要な違いは、オブジェクトは一方向のマッピングであるということです。つまり、キーからキー値にマッピングすることしかできません。一方、列挙型は双方向のマッピングであり、列挙メンバーから列挙値にマッピングすることも、列挙値から列挙メンバーにマッピングすることもできます:

enum Items {
  Foo,
  Bar,
  Baz
}

const fooValue = Items.Foo; // 0
const fooKey = Items[0]; // "Foo"

ただし、数値の列挙メンバーのみがこのような双方向列挙を行うことができ、文字列の列挙メンバーは依然として単方向のマッピングのみを行います

定数列挙#

const enum Items {
  Foo,
  Bar,
  Baz
}

const fooValue = Items.Foo; // 0

これは通常の列挙型との違いは、アクセス性とコンパイル生成物にあります。定数列挙では、列挙メンバーを通じてのみ列挙値にアクセスでき(値を通じてメンバーにアクセスすることはできません)、そのため、定数列挙はより効率的です。

関数#

関数の型シグネチャ#

関数の型は、関数の引数の型と関数の戻り値の型を記述します。

function foo(name: string): number {
  return name.length;
}

const foo = function (name: string): number {
  return name.length
}

const foo: (name: string) => number = function (name) {
  return name.length
}

const foo = (name: string): number => {
  return name.length
}

const foo: (name: string) => number = (name) => {
  return name.length
}

引数と戻り値の型を関数内で直接宣言するか、型エイリアスを使用して関数宣言を抽出することができます。

type FuncFoo = (name: string) => number

const foo: FuncFoo = (name) => {
  return name.length
}

この関数の型構造を記述するだけであれば、interface を使用して関数宣言を行うこともできます:

interface FuncFooStruct {
  (name: string): number
}

この時の interface は Callable Interface と呼ばれ、少し奇妙に見えるかもしれませんが、interface は型構造を記述するために使用され、関数型は本質的に構造が固定された型だからです。

オプション引数と rest 引数#

// 関数ロジック内でオプション引数のデフォルト値を注入
function foo1(name: string, age?: number): number {
  const inputAge = age || 18; // または age ?? 18 を使用
  return name.length + inputAge
}

// オプション引数にデフォルト値を直接指定
function foo2(name: string, age: number = 18): number {
  const inputAge = age;
  return name.length + inputAge
}

注意すべき点は、オプション引数は必須引数の後に配置しなければならないということです。結局、JavaScript では関数の引数は位置(形参)に基づいて渡され、引数名(名参)に基づいて渡されるわけではありません。

rest 引数の型注釈も比較的簡単です。実際には配列であるため、ここでも配列型またはタプル型を使用して注釈を付けるべきです:

function foo(arg1: string, ...rest: any[]) { }
function foo(arg1: string, ...rest: [number, boolean]) { }

オーバーロード#

引数に関連する戻り値の型を実現するために、TypeScript が提供する ** 関数オーバーロードシグネチャ(Overload Signature)** を使用できます。

function func(foo: number, bar: true): string;
function func(foo: number, bar?: false): number;
function func(foo: number, bar?: boolean): string | number {
  if (bar) {
    return String(foo);
  } else {
    return foo * 599;
  }
}

const res1 = func(599); // number
const res2 = func(599, true); // string
const res3 = func(599, false); // number

ここで注意すべき点は、複数のオーバーロード宣言を持つ関数は呼び出されるとき、オーバーロードの宣言順に従って下に検索されるということです。したがって、最初のオーバーロード宣言では、ロジックと一致させるために、bar が true のときに string 型を返すように、最初のオーバーロード宣言の bar を必須のリテラル型として宣言する必要があります。

実際、TypeScript のオーバーロードは擬似オーバーロードのようなもので、具体的な実装は一つだけであり、オーバーロードはメソッド呼び出しのシグネチャに体現され、具体的な実装には体現されません。C++ などの言語では、オーバーロードは引数が異なる同じ名前の関数の実装に体現され、これがより広義の関数オーバーロードです。

非同期関数#

非同期関数(つまり async とマークされた関数)の戻り値は必ず Promise 型であり、Promise 内部に含まれる型はジェネリクスの形式で書かれます。つまり Promise<T> です。

async function asyncFunc(): Promise<void> {}

クラス#

クラスとクラスメンバーの型シグネチャ#

関数の主要な構造は引数、ロジック、戻り値であり、ロジックの型注釈は実際には通常のコードの注釈に過ぎないため、引数と戻り値の型注釈のみを紹介しました。クラスでも同様で、主要な構造は ** コンストラクタ、プロパティ、メソッド、アクセス修飾子(Accessor)** だけです。

class Foo {
  prop: string;

  constructor(inputProp: string) {
    this.prop = inputProp;
  }

  print(addon: string): void {
    console.log(`${this.prop} and ${addon}`)
  }

  get propA(): string {
    return `${this.prop}+A`;
  }

  set propA(value: string) {
    this.prop = `${value}+A`
  }
}

唯一注意すべき点は、setter メソッドには戻り値の型注釈を付けることができないということです。setter の戻り値は消費されないため、プロセスにのみ焦点を当てた関数と考えることができます。クラスのメソッドも関数と同様にオーバーロードを行うことができ、構文は基本的に同じです。ここでは詳しく説明しません。

修飾子#

TypeScript では、クラスメンバーに次の修飾子を追加できます:public / private / protected / readonly。readonly 以外の他の三つはアクセス修飾子に属し、readonly は操作修飾子に属します(interface 内の readonly と同じ意味です)。アクセス修飾子を明示的に使用しない場合、メンバーのアクセス性はデフォルトで public にマークされます。

  • public:このクラスメンバーはクラス、クラスのインスタンス、サブクラスからアクセスできます。
  • private:このクラスメンバーはクラス内部でのみアクセスできます。
  • protected:このクラスメンバーはクラスとサブクラス内でのみアクセスできます。クラスとクラスのインスタンスを二つの概念として考えることができます。つまり、一度インスタンス化されると(工場の部品)、クラス(工場)とは関係がなくなり、保護されたメンバーにアクセスすることはできません。
class Foo {
  private prop: string;

  constructor(inputProp: string) {
    this.prop = inputProp;
  }

  protected print(addon: string): void {
    console.log(`${this.prop} and ${addon}`)
  }

  public get propA(): string {
    return `${this.prop}+A`;
  }

  public set propA(value: string) {
    this.propA = `${value}+A`
  }
}

通常、コンストラクタに修飾子を追加することはなく、デフォルトの public のままにします。

簡単にするために、コンストラクタ内で引数にアクセス修飾子を適用することができます:

class Foo {
  constructor(public arg1: string, private arg2: boolean) { }
}

new Foo("linbudu", true)

静的メンバー#

static キーワードを使用して、メンバーを静的メンバーとしてマークできます:

class Foo {
  static staticHandler() { }

  public instanceHandler() { }
}

インスタンスメンバーとは異なり、クラス内部の静的メンバーには this を使用してアクセスすることはできず、Foo.staticHandler のような形式でアクセスする必要があります。
静的メンバーはインスタンスに継承されず、常に現在定義されているこのクラス(およびそのサブクラス)にのみ属します。

継承、実装、抽象クラス#

class Base { }

class Derived extends Base { }

基底クラスのどのメンバーが派生クラスからアクセスできるかは、完全にそのアクセス修飾子によって決まります。上記で紹介したように、派生クラスは public または protected 修飾子を持つ基底クラスメンバーにアクセスできます。アクセスの他に、基底クラスのメソッドは派生クラスでオーバーライドすることもできますが、super を使用して基底クラスのメソッドにアクセスすることもできます:

class Base {
  print() { }
}

class Derived extends Base {
  print() {
    super.print()
    // ...
  }
}

派生クラスで基底クラスのメソッドをオーバーライドする際、派生クラスのこのメソッドが基底クラスのメソッドをオーバーライドできることを保証することはできません。なぜなら、基底クラスにこのメソッドが存在しない可能性があるからです。したがって、TypeScript 4.3 では override キーワードが追加され、派生クラスがオーバーライドしようとするメソッドが基底クラスに必ず定義されていることを保証します:

class Base {
  printWithLove() { }
}

class Derived extends Base {
  override print() {
    // ...
  }
}

基底クラスと派生クラスの他に、もう一つ重要な概念があります:抽象クラス。抽象クラスはクラスの構造とメソッドの抽象化です。簡単に言えば、抽象クラスはクラスにどのようなメンバー(プロパティ、メソッドなど)が必要かを記述し、抽象メソッドはこのメソッドが実際の実装でどのような構造を持つべきかを記述します。クラスのメソッドと関数は非常に似ており、構造を含むため、抽象メソッドはこのメソッドの引数の型戻り値の型を記述します。

abstract class AbsFoo {
  abstract absProp: string;
  abstract get absGetter(): string;
  abstract absMethod(name: string): string
}
class Foo implements AbsFoo {
  absProp: string = "linbudu"

  get absGetter() {
    return "linbudu"
  }

  absMethod(name: string) {
    return name
  }
}

この時、私たちはこの抽象クラスのすべての抽象メンバーを完全に実装しなければなりません。注意すべき点は、TypeScript では静的な抽象メンバーを宣言することはできません

interface は関数構造を宣言するだけでなく、クラスの構造を宣言することもできます:

interface FooStruct {
  absProp: string;
  get absGetter(): string;
  absMethod(input: string): string
}

class Foo implements FooStruct {
  absProp: string = "linbudu"

  get absGetter() {
    return "linbudu"
  }

  absMethod(name: string) {
    return name
  }
}

さらに、Newable Interface を使用してクラスの構造を記述することもできます(Callable Interface の関数構造を記述するのに似ています):

class Foo { }

interface FooStruct {
  new(): Foo
}

declare const NewableFoo: FooStruct;

const foo = new NewableFoo();

any 型#

any 型としてマークされた引数は任意の型の値を受け入れることができ、宣言後に再び任意の型の値を受け入れることができ、任意の他の型の変数に代入することができます:

// any 型としてマークされた変数は任意の型の値を持つことができます
let anyVar: any = "linbudu";

anyVar = false;
anyVar = "linbudu";
anyVar = {
  site: "juejin"
};

anyVar = () => { }

// 具体的な型としてマークされた変数も任意の any 型の値を受け入れることができます
const val1: string = anyVar;
const val2: number = anyVar;
const val3: () => {} = anyVar;
const val4: {} = anyVar;

any 型の変数に対しては、代入、アクセス、メソッド呼び出しなど、任意の操作を行うことができ、ここでは型推論とチェックが完全に無効化されていると考えることができます:

let anyVar: any = null;

anyVar.foo.bar.baz();
anyVar[0][1][2].prop1;

any 型の主な意味は、無拘束の「任意の型」を表し、すべての型と互換性があり、すべての型に互換性を持つことができるということです。
any の本質は型システムにおけるトップ型、すなわち最上位の型です。

  • 型不一致のエラーが発生して any を使用することになった場合、型アサーションを代わりに使用することを検討してください。
  • 型が複雑すぎてすべてを宣言したくないために any を使用する場合、その部分の型を必要な最小の型にアサートすることを検討してください。例えば、foo.bar.baz() を呼び出す必要がある場合、まず foo を bar メソッドを持つ型にアサートすることができます。
  • 未知の型を表現したい場合、より合理的な方法は unknown を使用することです。

unknown 型#

unknown 型の変数は任意の他の型に再代入できますが、any と unknown 型の変数にのみ代入できます:

let unknownVar: unknown = "linbudu";

unknownVar = false;
unknownVar = "linbudu";
unknownVar = {
  site: "juejin"
};

unknownVar = () => { }

const val1: string = unknownVar; // エラー
const val2: number = unknownVar; // エラー
const val3: () => {} = unknownVar; // エラー
const val4: {} = unknownVar; // エラー

const val5: any = unknownVar;
const val6: unknown = unknownVar;

unknown と any の主な違いは、他の変数に代入する際に現れます。any は「私は万千に化身し、どこにでも存在する」と見なされ、すべての型がそれを自分の仲間と見なします。一方、unknown は「私は万千に化身しているが、将来的に確定した型を得ると信じている」と見なされ、any と unknown 自身だけがそれを自分の仲間と見なします。簡単に言えば、any はすべての型チェックを放棄し、unknown はそうではありません。この点は、unknown 型の変数に対してプロパティアクセスを行う際にも現れます:

let unknownVar: unknown;
unknownVar.foo(); // エラー:オブジェクト型は unknown です

unknown 型に対してプロパティアクセスを行うには、型アサーションが必要です。つまり「これは未知の型ですが、ここではこの型であると保証します!」ということです:

let unknownVar: unknown;
(unknownVar as { foo: () => {} }).foo();

型が未知である場合、unknown を使用して注釈を付けることをお勧めします。これは、あなたが各所で型の構造を保証するために追加の精神的負担を使用していることを意味し、後で具体的な型にリファクタリングする際にも最初の型情報を得ることができ、型チェックの存在も保証されます。

never 型#

never 型は何の型情報も持たないため、ユニオン型では直接削除されます。

declare let v1: never;
declare let v2: void;

v1 = v2; // X 型 void は型 never に割り当てることはできません
v2 = v1;

プログラミング言語の型システムにおいて、never 型は ボトム型(Bottom Type) と呼ばれ、型システム階層の中で最も下位の型です。null や undefined と同様に、すべての型のサブタイプですが、never 型の変数のみが別の never 型の変数に代入できます。

通常、never 型を明示的に宣言することはありませんが、特定の状況で never を使用することは論理的に適切です。例えば、エラーをスローするだけの関数:

function justThrow(): never {
  throw new Error()
}

型の流れの分析において、戻り値の型が never の関数が呼び出されると、その下のコードはすべて無効なコード(つまり実行されない)と見なされます:

function foo (input:number){
  if(input > 1){
    justThrow();
    // return 文の後のコードと同等で、つまりデッドコード
    const name = "linbudu";
  }
}

また、型チェックを利用して明示的に使用することもできます。つまり、上記のユニオン型で never 型が神秘的に消える理由です。

if (typeof strOrNumOrBool === "string") {
    // これは必ず文字列です!
  strOrNumOrBool.charAt(1);
} else if (typeof strOrNumOrBool === "number") {
  strOrNumOrBool.toFixed();
} else if (typeof strOrNumOrBool === "boolean") {
  strOrNumOrBool === true;
} else {
  const _exhaustiveCheck: never = strOrNumOrBool;
  throw new Error(`Unknown input type: ${_exhaustiveCheck}`);
}

仮に、ある不注意な同僚が新しいタイプの分岐を追加し、strOrNumOrBoolstrOrNumOrBoolOrFunc に変わり、対応する処理分岐を追加するのを忘れた場合、この else コードブロック内で never 型の変数に Function 型を割り当てる型エラーが発生します。これは、型分析能力と never 型が never 型の変数にのみ割り当てられるという点を利用して、ユニオン型変数が適切に処理されることを保証するものです。

型アサーション#

型アサーションは、型チェックプログラムに現在の変数の型を明示的に通知することができ、型分析を修正したり、型を変更したりする操作です。これは、変数の既存の型を新しく指定された型に変更する操作であり、基本的な構文は as NewType です。any /unknown 型を具体的な型にアサートすることができます:

const str: string = "linbudu";
(str as any).func().foo().prop;

function foo(union: string | number) {
  if ((union as string).includes("linbudu")) { }
  if ((union as number).toFixed() === '599') { }
}

ただし、型アサーションの正しい使用方法は、TypeScript の型分析が不正確または期待に反する場合に、正しい型にアサートすることです:

interface IFoo {
  name: string;
}

declare const obj: {
  foo: IFoo
}

const {
  foo = {} as IFoo
} = obj

二重アサーション#

あなたのアサーション型と元の型の差が大きすぎる場合、一般的なクラス(つまり any /unknown)にアサートする必要があります。この一般的な型はすべての可能な型を含むため、それにアサートすることそれから別の型にアサートすることの差はほとんどありません。

const str: string = "linbudu";

(str as unknown as { handler: () => {} }).handler();

// 尖括弧アサーションを使用
(<{ handler: () => {} }>(<unknown>str)).handler();

非空アサーション#

非空アサーションは実際には型アサーションの簡略化で、! 構文を使用します。つまり、obj!.func()!.prop の形式で前の宣言が必ず非空であることを示します(実際には null と undefined 型を除外したことになります)。例えば、この例:

declare const foo: {
  func?: () => ({
    prop?: number | null;
  })
};

foo.func!().prop!.toFixed();

非空アサーションの一般的なシーンには document.querySelectorArray.find メソッドなどがあります:

const element = document.querySelector("#id")!;
const target = [1, 2, 3, 599].find(item => item === 599)!;

型アサーションのもう一つの使い方は、コードヒントの補助ツールとして使用することです。例えば、以下の少し複雑なインターフェース:

interface IStruct {
  foo: string;
  bar: {
    barPropA: string;
    barPropB: number;
    barMethod: () => void;
    baz: {
      handler: () => Promise<void>;
    };
  };
}

この構造に基づいて随意にオブジェクトを実装したい場合、型注釈を使用することができます:

const obj: IStruct = {};

この時、あなたを待っているのは一連の型エラーであり、インターフェース構造全体を規則正しく実装する必要があります。しかし、型アサーションを使用すれば、型ヒントを保持しながら、あまり完全にこの構造を実装しなくても済みます:

// この例はエラーになりません
const obj = <IStruct>{
  bar: {
    baz: {},
  },
};

References:
https://juejin.cn/book/7086408430491172901

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。