TypeScriptの復習しまーす
演算子|でプリミティブ型を並べることで、それらの内のいずれの型でも許容すると言う意味になる。
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 }
演算子&で型を並べることで、「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 }
注意する点はこんな感じ
オプショナルのプロパティにアクセスしようとするとtypescriptが「ここはnullの可能性がありますよ」とコンパイルエラーを吐く。
このコンパイルエラーを強制的に黙らせる方法として非Nullアサーション演算子(!)を使用すると言う方法がある。
これは「ここには絶対に null も undefined も入りませんよ」とコンパイラを強引に黙らせるもの。
ただこれはせっかくの null 安 全性を壊すもので、実際に値が null や undefined だったら実行時エラーになる。だから余程の保証がない限りは使うべきではない。
通常の式では渡された値の型の名前を文字列として返す。
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!
通常の式では指定した値がオブジェクトのキーとして存在するかどうかの真偽値を返す。
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!
型の文脈のみで用いられる演算子。オブジェクトの型からキーを抜き出すことができる。
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 ... 型の文脈のみで用いられる演算子。オブジェクトの型からキーを抜き出すことができる。
型が分からない値を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
)
}
こんな感じでただのオブジェクトに対して事前に型を絞り込む方法を「ユーザー定義の型ガード」と呼ぶ。
今日はTypeScriptの色々を復習しました〜
最後までごらんいただきありがとうございました~!
@ 酒井悠宇