Desmond

Desmond

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

TypeScript 型プログラミング

型別エイリアス#

一組のユニオン型を抽出する:

type StatusCode = 200 | 301 | 400 | 500 | 502;
type PossibleDataTypes = string | number | (() => unknown);

const status: StatusCode = 502;

関数型を抽出する:

type Handler = (e: Event) => void;

const clickHandler: Handler = (e) => { };
const moveHandler: Handler = (e) => { };
const dragHandler: Handler = (e) => { };

型エイリアスでは、型エイリアスがジェネリックを受け入れることを宣言できます(これをジェネリックプレースホルダーと呼びます)。ジェネリックを受け入れると、それをユーティリティ型と呼びます:

type MaybeNull<T> = T | null;
function process(input: MaybeNull<{ handler: () => {} }>) {
  input?.handler();
}
type MaybeArray<T> = T | T[];

// 関数のジェネリックは後で学びます~
function ensureArray<T>(input: MaybeArray<T>): T[] {
  return Array.isArray(input) ? input : [input];
}

ユニオン型とインターセクション型#

実際、ユニオン型の記号は|であり、これはビット単位の OR を表します。つまり、ユニオン型の中の 1 つの型に一致すれば、そのユニオン型を実装したと見なされます。例えばA | Bでは、A または B のいずれかを実装すればよいのです。

一方、ビット単位の AND を表す&は異なります。ここにあるすべての型に一致する必要があり、そうでなければこのインターセクション型を実装したとは言えません。つまり、A & Bでは、A と B の両方の型を満たす必要があります

interface NameStruct {
  name: string;
}

interface AgeStruct {
  age: number;
}

type ProfileStruct = NameStruct & AgeStruct;

const profile: ProfileStruct = {
  name: "linbudu",
  age: 18
}
type StrAndNum = string & number; // never
type UnionIntersection1 = (1 | 2 | 3) & (1 | 2); // 1 | 2
type UnionIntersection2 = (string | number | symbol) & string; // string

インデックス型#

インデックスシグネチャ型#

インデックスシグネチャ型は、主にインターフェースや型エイリアス内で、以下の構文を使用してキーと値の型が一致する型構造を迅速に宣言することを指します:

interface AllStringTypes {
  [key: string]: string;
}

type PropType1 = AllStringTypes['linbudu']; // string
type PropType2 = AllStringTypes['599']; // string

しかし、JavaScript では、obj[prop]形式のアクセスが数値インデックスアクセスを文字列インデックスアクセスに変換するため、obj[599]obj['599']の効果は同じです。したがって、文字列インデックスシグネチャ型内でも数値型のキーを宣言できます。同様に、symbol 型も同様です:

const foo: AllStringTypes = {
  "linbudu": "599",
  599: "linbudu",
  [Symbol("ddd")]: 'symbol',
}

インデックスシグネチャ型は、具体的なキーと値の型の宣言と共存できますが、この場合、これらの具体的なキーと値の型もインデックスシグネチャ型の宣言に従う必要があります:

interface AllStringTypes {
  // 型“number”的属性“propA”不能赋给“string”索引类型“boolean”。
  propA: number;
  [key: string]: boolean;
}

interface StringOrBooleanTypes {
  propA: number;
  propB: boolean;
  [key: string]: number | boolean;
}

インデックスシグネチャ型の一般的なシナリオは、JavaScript コードをリファクタリングする際に、内部プロパティが多いオブジェクトに対して any のインデックスシグネチャ型を宣言し、型が明確でないプロパティへのアクセスを一時的にサポートすることです。そして、後で少しずつ型を補完します:

interface AnyTypeHere {
  [key: string]: any;
}

const foo: AnyTypeHere['linbudu'] = 'any value';

インデックス型クエリ#

keyof 演算子は、オブジェクト内のすべてのキーを対応するリテラル型に変換し、それをユニオン型に組み合わせます。注意すべきは、ここでは数値型のキー名を文字列型リテラルに変換せず、数値型リテラルのまま保持することです。

interface Foo {
  linbudu: 1,
  599: 2
}

type FooKeys = keyof Foo; // "linbudu" | 599
// VS Codeでマウスをホバーすると「keyof Foo」しか見えません
// 実際の値は見えませんが、次のようにできます:
type FooKeys = keyof Foo & {}; // "linbudu" | 599

インデックス型アクセス#

インデックス型クエリの本質は、キーのリテラル型を使用して、そのキーに対応する値の型にアクセスすることです。

interface NumberRecord {
  [key: string]: number;
}

type PropType = NumberRecord[string]; // number
interface Foo {
  propA: number;
  propB: boolean;
}

type PropAType = Foo['propA']; // number
type PropBType = Foo['propB']; // boolean
interface Foo {
  propA: number;
  propB: boolean;
  propC: string;
}

type PropTypeUnion = Foo[keyof Foo]; // string | number | boolean

マッピング型#

マッピング型の主な目的は、キー名に基づいてキー値の型をマッピングすることです。

type Stringify<T> = {
  [K in keyof T]: string;
};

interface Foo {
  prop1: string;
  prop2: number;
  prop3: boolean;
  prop4: () => void;
}

type StringifiedFoo = Stringify<Foo>;

// 等価
interface StringifiedFoo {
  prop1: string;
  prop2: string;
  prop3: string;
  prop4: string;
}

このユーティリティ型は、オブジェクト型を受け取り(仮に私たちがそう使うと仮定します)、keyof を使用してこのオブジェクト型のキー名からリテラルユニオン型を取得し、マッピング型(ここでの in キーワード)を使用してこのユニオン型の各メンバーをマッピングし、そのキー値の型を string に設定します。

type Clone<T> = {
  [K in keyof T]: T[K];
};

ここでのT[K]は、上記で述べたインデックス型アクセスであり、キーのリテラル型を使用してキー値の型にアクセスしています。これはインターフェースをクローンすることに相当します。注意すべきは、ここでのK inはマッピング型の構文であり、keyof Tは keyof 演算子、[K in keyof T][]はインデックスシグネチャ型、T[K]はインデックス型アクセスに属します。

image

型クエリ演算子 Type Query Operator#

const str = "linbudu";

const obj = { name: "linbudu" };

const nullVar = null;
const undefinedVar = undefined;

const func = (input: string) => {
  return input.length > 10;
}

type Str = typeof str; // "linbudu"
type Obj = typeof obj; // { name: string; }
type Null = typeof nullVar; // null
type Undefined = typeof undefined; // undefined
type Func = typeof func; // (input: string) => boolean
const func = (input: string) => {
  return input.length > 10;
}

const func2: typeof func = (name: string) => {
  return name === 'linbudu'
}
const func = (input: string) => {
  return input.length > 10;
}

// boolean
type FuncReturnType = ReturnType<typeof func>;

ほとんどの場合、typeof が返す型は、変数名にマウスをホバーしたときに表示される推論された型であり、** 最も狭い推論レベル(すなわちリテラル型のレベルまで)** です。この 2 つの typeof を混同することを心配する必要はありません。論理コードで使用される typeof は必ず JavaScript の typeof であり、型コード(型注釈、型エイリアスなど)内のものは必ず型クエリの typeof です。また、このような状況を避けるために、型クエリ演算子の後には式を使用することは許可されていません:

const isInputValid = (input: string) => {
  return input.length > 10;
}

// 式は許可されません
let isValid: typeof isInputValid("linbudu");

型ガード#

TypeScript は非常に強力な型推論能力を提供しており、コードのロジックに応じて型を狭めることを試みます。この能力は型の制御フロー分析と呼ばれ、プログラミング言語の型能力の中で最も重要な部分です:実際のロジックと密接に関連した型。私たちはロジックから型を推論し、逆に型がロジックを保護します。

function isString(input: unknown): boolean {
  return typeof input === "string";
}

function foo(input: string | number) {
  if (isString(input)) {
    // 型“string | number”上不存在属性“replace”。
    (input).replace("linbudu", "linbudu599")
  }
  if (typeof input === 'number') { }
  // ...
}

isString 関数は別の場所にあり、内部の判断ロジックは関数 foo 内にはありません。ここでの型制御フロー分析は、関数間のコンテキストを越えて型の情報を収集することはできません(他の型言語ではサポートされているかもしれません)。

実際、判断ロジックを外部に抽出して再利用することは非常に一般的です。この型制御フロー分析の能力不足を解決するために、TypeScript はisキーワードを導入して明示的に型情報を提供します:

function isString(input: unknown): input is string {
  return typeof input === "string";
}

function foo(input: string | number) {
  if (isString(input)) {
    // 正しくなりました
    (input).replace("linbudu", "linbudu599")
  }
  if (typeof input === 'number') { }
  // ...
}

is string、つまりis キーワード + 期待される型です。この関数が true を返すと、is キーワードの前の引数の型は、この型ガードの呼び出し側の後続の型制御フロー分析に収集されます

注意すべきは、型ガード関数内では判断ロジックと実際の型の関連をチェックしないことです:

function isString(input: unknown): input is number {
  return typeof input === "string";
}

function foo(input: string | number) {
  if (isString(input)) {
    // エラー、ここではnumber型になっています
    (input).replace("linbudu", "linbudu599")
  }
  if (typeof input === 'number') { }
  // ...
}

この観点から見ると、型ガードは型アサーションに似ていますが、型ガードはより寛容で、あなたをより信頼しています。あなたが指定した型は、そのままその型になります。

export type Falsy = false | "" | 0 | null | undefined;

export const isFalsy = (val: unknown): val is Falsy => !val;

// あまり使われないsymbolとbigintを除く
export type Primitive = string | number | boolean | undefined;

export const isPrimitive = (val: unknown): val is Primitive => ['string', 'number', 'boolean' , 'undefined'].includes(typeof val);

in と instanceof に基づく型保護#

interface Foo {
  foo: string;
  fooOnly: boolean;
  shared: number;
}

interface Bar {
  bar: string;
  barOnly: boolean;
  shared: number;
}

function handle(input: Foo | Bar) {
  if ('foo' in input) {
    input.fooOnly;
  } else {
    input.barOnly;
  }
}

function handle(input: Foo | Bar) {
  if ('shared' in input) {
    // 型“Foo | Bar”上不存在属性“fooOnly”。型“Bar”上不存在属性“fooOnly”。
    input.fooOnly;
  } else {
    // 型“never”上不存在属性“barOnly”。
    input.barOnly;
  }
}

この時、誰かが「なぜ shared を使えないのか?」と尋ねるかもしれません。明らかに、それは両方の型に同時に存在するため、識別能力がありません。一方、foo /bar および fooOnly /barOnly は各型に固有の属性であるため、** 識別可能な属性(Discriminant Property または Tagged Property)** として機能します。Foo と Bar は、このような識別能力を持つ属性が存在するため、** 識別可能なユニオン型(Discriminated Unions または Tagged Union)** と呼ばれます。これらは一連の型のユニオンですが、その中の各型は独自のユニークな属性を持ち、それによって際立っています。

この識別可能な属性は、構造的なものである可能性があります。例えば、構造 A の属性 prop が配列であり、構造 B の属性 prop がオブジェクトである場合、または構造 A に属性 prop が存在し、構造 B には存在しない場合です。

それは、共通の属性のリテラル型の違いである可能性もあります:

function ensureArray(input: number | number[]): number[] {
  if (Array.isArray(input)) {
    return input;
  } else {
    return [input];
  }
}

interface Foo {
  kind: 'foo';
  diffType: string;
  fooOnly: boolean;
  shared: number;
}

interface Bar {
  kind: 'bar';
  diffType: number;
  barOnly: boolean;
  shared: number;
}

function handle1(input: Foo | Bar) {
  if (input.kind === 'foo') {
    input.fooOnly;
  } else {
    input.barOnly;
  }
}

function handle2(input: Foo | Bar) {
  // エラー、区別の役割を果たしていません。2つのコードブロックの中でFoo | Barです。
  if (typeof input.diffType === 'string') {
    input.fooOnly;
  } else {
    input.barOnly;
  }
}
class FooBase {}

class BarBase {}

class Foo extends FooBase {
  fooOnly() {}
}
class Bar extends BarBase {
  barOnly() {}
}

function handle(input: Foo | Bar) {
  if (input instanceof FooBase) {
    input.fooOnly();
  } else {
    input.barOnly();
  }
}

型アサーションガード#

アサーションガードと型ガードの最大の違いは、判断条件が通らない場合、アサーションガードはエラーをスローする必要があるのに対し、型ガードは期待される型を除外するだけです。

function assert(condition: any, msg?: string): asserts condition {
  if (!condition) {
    throw new Error(msg);
  }
}

ここで使用されているのはasserts conditionであり、condition は実際のロジックから来ています!これは、condition というロジックレベルのコードを、型レベルの判断基準として使用していることを意味します。これは、戻り値の型に論理式を使用して型注釈を行ったことに相当します。

let name: any = 'linbudu';

function assertIsNumber(val: any): asserts val is number {
  if (typeof val !== 'number') {
    throw new Error('Not a number!');
  }
}

assertIsNumber(name);

// number型!
name.toFixed();

この場合、アサーションガードに式を渡す必要はなく、その判断に使用する式をアサーションガードの内部に置くことで、より独立したコードロジックを得ることができます。


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

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