2021/08/14

TypeScript復習

@ 酒井悠宇

TypeScript

TypeScriptの復習しまーす

共用体型と交差型


共用体型(UnionTypes、ユニオン型)


演算子|でプリミティブ型を並べることで、それらの内のいずれの型でも許容すると言う意味になる。

let id: number | string = 11111;
id //11111
id =  "あああああ"
id //"あああああ"


オブジェクト型を共用体型にすることもできる。
プリミティブ型と同じでいずれのオブジェクトの型でも許容するという意味。

type A = {
  foo: number;
  bar?: string;
};
type B = { foo: string };
type C = { bar: string };
type D = { baz: boolean };
type AorB = A | B; // { foo: number | string; bar?: string }
type AorC = A | C; // { foo: number; bar?: string } or { bar: string }
type AorD = A | D; // { foo: number; bar?: string } or { baz: boolean }


交差型(IntersectionTypes、インターセクション型)


演算子&で型を並べることで、「A かつ B」である型と言う意味になる。オブジェクト型の合成に使われる。プリミティブ型には使われない。

プリミティブ型で使用されないのは、「number型 かつ string型」と言う型が存在し得ないから。


オブジェクトの交差型は以下のように使用する。

type A = { foo: number };
type B = { bar: string };
type C = {
  foo?: number;
  baz: boolean;
};
type AnB = A & B; // { foo: number, bar: string }
type AnC = A & C; // { foo: number, baz: boolean }
type CnAorB = C & (A | B);
// { foo: number, baz: boolean } or { foo?: number, bar: string, baz: boolean }

注意する点はこんな感じ

  • AnC 型の foo プロパティのように、同じ型でありながら必須と省略可能が交差したら、必須のほうが優先される。
  • もし同じプロパティで型が共通点のないものだった場合は、never 型になる。


非 Null アサーション演算子(!)


オプショナルのプロパティにアクセスしようとするとtypescriptが「ここはnullの可能性がありますよ」とコンパイルエラーを吐く。
このコンパイルエラーを強制的に黙らせる方法として非Nullアサーション演算子(!)を使用すると言う方法がある。
これは「ここには絶対に null も undefined も入りませんよ」とコンパイラを強引に黙らせるもの。
ただこれはせっかくの null 安 全性を壊すもので、実際に値が null や undefined だったら実行時エラーになる。だから余程の保証がない限りは使うべきではない。

型表現に使われる演算子


typeof演算子

通常の式では渡された値の型の名前を文字列として返す。

console.log(typeof 100); // 'number'

const arr = [1, 2, 3];
console.log(typeof arr); // 'object'


型の文脈で用いると変数から型を抽出してくれる。

type NumArr = typeof arr; //型推論でnuber[]型を抽出してくれる。
const val: NumArr = [4, 5, 6];
const val2: NumArr = ["foo", "bar", "baz"]; // compile error!


in演算子

通常の式では指定した値がオブジェクトのキーとして存在するかどうかの真偽値を返す。

const obj = { a: 1, b: 2, c: 3 };
console.log('a' in obj); // objと言うオブジェクトの中にaと言うキーは存在する?
// true


for...in文では、オブジェクトからキーを抽出するのに使われる。

for (const key in obj) { console.log(key); } // a b c


※for...in文の一般的な構文。

for( const 変数 in オブジェクト ) {
 
    //ここに繰り返し処理を書く
 
}

オブジェクトのキーが一つずつ変数に格納されて処理を行うことができる。

型の文脈 で使用すると、列挙された型の中から各要素の型の値を抜き出して マップ型(Mapped Types)というものを作る。

type Fig = "one" | "two" | "three"; //文字列リテラルの共用体型
type FigMap = { [k in Fig]?: number }; //マップ型を作成

const figMap: FigMap = {
  one: 1,
  two: 2,
  three: 3,
};

figMap.four = 4; // compile error!


keyof演算子


型の文脈のみで用いられる演算子。オブジェクトの型からキーを抜き出すことができる。

type Foo = {
  a: number;
  b: string;
};

type A = keyof Foo; 


keyof typeof と繋げると、オブジェクトのキーの型を出力することができる。

const permissions = {
  r: 0b100,
  w: 0b010,
  x: 0b001,
};
type PermsChar = keyof typeof permissions; // 'r' | 'w' | 'x'
const readable: PermsChar = "r";
const writable: PermsChar = "z"; // compile error!


TypeScriptにはvalueof演算子はないので、バリューを抜き出したい場合はインデックスアクセス演算子[]を使うことでやりたいことができる。

型表現に使われる演算子まとめ


それぞれの演算子を型の文脈で使用した場合の挙動をまとめます。

typeof ... 渡された値から型を抽出する
in ... 列挙された型の中から各要素の型の値を抜き出して マップ型(Mapped Types)というものを作る。
keyof ... 型の文脈のみで用いられる演算子。オブジェクトの型からキーを抜き出すことができる。

型アサーションと型ガード


asによる型アサーション


型が分からない値をTypeScriptでうまく扱う一番手っ取り早い解決方法として、型アサーション(Tyep Assertion)を行うと言う手段がある。
型アノテーションと似てるけど単語の意味としては、「annotation」が「注釈」で、「assertion」が「断定」。
「これは〇〇型ですよ~」がアノテーションだとしたら、「いやこれは絶対〇〇型だから!」がアサーション。

アサーションした型がうまく適合している限りは問題なく動くが、適合していない場合は以下のようなことが起きてしまう。

type User = { username: string; address: { zipcode: string; town: string } };
const str = `{ "username": "patty", "town": "Maple Town" }`;
const data: unknown = JSON.parse(str);
const user = data as User;
console.log(user.address.town); // TypeError: Cannot read property 'town' of undefined(実行時のエラー)


コンパイル時にはエラーを吐かないが、実行時にエラーが出てしまう。
なので、型アサーションは根拠なく開発者の判断がまかり通ってしまう、型安全性が全く保証されていない方法であると言える。

型キャストと型アサーション


型キャストと型アサーションは別物

export const n = 123;
const s1 = String(n); //型キャスト
console.log(typeof s1);
//string
const s2 = n as string; //型アサーション
// error


前半で行っている型キャストは、異なるデータ型の値を任意の型にコンバート(変換)するもの。
後半の型アサーションはあくまでコンパイラによる型の解釈が変わるだけであって、実際の値が変化するわけではない。

型キャストで数値の123を文字列の"123"にすることは可能だが、
誰がどうみてもリテラルnumber型の123に対して、型アサーションで「いやこれstring型だぜ!」って言えば当然エラーが出る。

型キャストと型アサーションの違いは、「実際の値を変えているか、型の解釈を変えているか」。

型ガード


あるスコープ内での型を保証するチェックを行う式のことを、「型ガード(Type Guards)」と言う。
例えば以下のような処理も型ガードの一種である。

export const foo: unknown = "1,2,3,4";
if (typeof foo === "string") {
  console.log(foo.split(","));
}
console.log(foo.split(",")); // compile error!


unknown型を当てた変数fooをtypeofによってstring型である場合にのみ、split();を使用できるようにしている。
このifのスコープ外でfooに対してsplitを使用しようとすると、fooはunknown型なので当然split()が使用できず、コンパイルエラーが出る。

より安全なコードを書くためには、コンパイルを通すために場当たり的に型アサーションを行うのではなく、型ガードを積極的に行っていくべき。

クラスのインスタンスであればinstanceofを使用して型ガードを実装することができる。

export class Base {
  common = "common";
}
class Foo extends Base {
  foo = () => {
    console.log("foo");
  };
}
class Bar extends Base {
  bar = () => {
    console.log("bar");
  };
}
const doDivide = (arg: Foo | Bar) => {
  if (arg instanceof Foo) { //引数がFooのインスタンスである場合に実行
    arg.foo();
    arg.bar(); // compile error!
  } else { //引数がBarのインスタンスである場合に実行
    arg.bar();
    arg.foo(); // compile error!
  }
  console.log(arg.common);
};
doDivide(new Foo());//インスタンス生成 & 関数呼び出し
doDivide(new Bar());


ただしクラスを下敷きにしていないただのオブジェクトではこの方法を使うことができないので、自前で型を絞り込むしくみを作ってあげる必要がある。
この方法は「ユーザー定義の型ガード 」と呼ばれる。

type User = { username: string; address: { zipcode: string; town: string } };
const isUser = (arg: unknown): arg is User => {
  const u = arg as User;
  return (
    typeof u?.username === "string" &&
    typeof u?.address?.zipcode === "string" &&
    typeof u?.address?.town === "string"
  );
};

const u1: unknown = JSON.parse("{}");
const u2: unknown = JSON.parse('{ "username": "patty", "address": "Maple Town" }');
const u3: unknown = JSON.parse(
  '{ "username": "patty", "address": { "zipcode": "111", "town": "Maple Town" } }'
);
[u1, u2, u3].forEach((u) => {
  if (isUser(u)) {
    console.log(`${u.username} lives in ${u.address.town}`);
  } else {
    console.log("It's not User");
    console.log(`${u.username} lives in ${u.address.town}`); // compile error!
  }
});


関数 isUser() の戻り値の型定義のarg is User と言う表現は、型述語(Type Predicate)という表現で、この関数が true を返す場合に引数 arg の型が User であると言うことをコンパイラに教えている的な意味になる。

↓この関数の返り値がtrueなら、

const isUser = (arg: unknown): arg is User => {
};


こんな感じになるよ!って感じ。

const inUser = (arg: User): boolean => {
  return(
    true
  )
}


こんな感じでただのオブジェクトに対して事前に型を絞り込む方法を「ユーザー定義の型ガード」と呼ぶ。

型アサーションと型ガードのまとめ


  • 型アサーションとは、プログラマがコンパイラの判断を無視して型を断定する方法のこと。
  • unknown型などの、型がわからない値をTypeScriptうまく扱うために、一番手っ取り早いのが型アサーションを使用して型を断定する方法。
  • しかし型アサーションは根拠なく開発者の判断がまかり通る、型安全性が全く保証されていない方法なので、「型ガード」を使用する方が良いとされている。
  • 型ガードとは、あるスコープ内での型を保証するチェックを行う式のこと。
  • プリミティブ型であればtypeofを使用してifの条件式で型を絞り込むことで型ガードを実装できる。
  • クラスのインスタンスであればinstanceofを使用して引数があるクラスのインスタンスであるかどうかを判断して型ガードを実装できる。
  • クラスが下敷きになっていないただのオブジェクトであれば、「ユーザー定義の型ガード」と言う型を絞り込むシステムを事前に作成する方法を使用して型ガードを実装できる。型述語(is)を使用して、引数に渡される型がある型であるかどうかを判断することができる。


今日はTypeScriptの色々を復習しました〜
最後までごらんいただきありがとうございました~!

© 2021 powerd by UnReact