はじめに
前回の TypeScriptやばいところ濃縮ジュースに引き続き、今回も勉強ノートです。
今回のお題はこちら
これをみてざっと検証してみました
実行環境
TypeScript 4.5.0-dev.20210903
いざ
早速いってみましょう。
まずは公式のサンプルを抜粋します。実装内容は表題の通り、構造が一緒でも名前が違えば「違う型に判定される」パターンを作っていきます。
// FOO
interface FooId extends String {
_fooIdBrand: string; // To prevent type errors
}
// BAR
interface BarId extends String {
_barIdBrand: string; // To prevent type errors
}
/**
* Usage Demo
*/
let fooId: FooId;
let barId: BarId;
// Safety!
fooId = barId; // error
barId = fooId; // error
fooId = <FooId>barId; // error
barId = <BarId>fooId; // error
// Newing up
fooId = 'foo' as any;
barId = 'bar' as any;
// If you need the base string
let str: string;
str = fooId as any;
str = barId as any;
さて、最後の二行。なぜanyにしないといけないのでしょうか。
ついstringにしたくなりますが...?
// If you need the base string
let str: string;
str = fooId as any;
str = barId as any;
// Can't do this
str = fooId as string; // error
やってみたらエラーになりました。もちろん、stringをstringに代入できないのではなく、「fooIdをstringにキャストできないよ」と怒られています。サブタイプには型アサーションできないですね。
型アサーションに必要な条件とは?
なんとなく、_fooIdBrandプロパティの存在が重要そうですが、他にいくつかの質問が浮かびます。
- typeだとどうなる?
- extendsは?
- プロパティ名同じで違う型は?
と言うわけで全部やってみました!
Interfaceで継承・拡張
/**
* More Examples
*/
// extending
interface FooFooId extends FooId {
_fooFooIdBrand: string; // Not _fooIdBrand
}
// more properties but not extending
interface FooBarId {
_fooBarIdBrand: string;
_fooIdBrand: string;
_barIdBrand: string;
}
fooId as AnotherFooId; // OK
fooId as FooFooId; // ok because FooId is a subtype of FooFooId
fooId as FooBarId; // error, FooId is not a subtype of FooBarId
明確に継承したインターフェイスは許容するみたいですね。ここは名前主義的です。しかし同じ構造の違う名前のインターフェイスは受け入れてしまっているので、まだ構造主義的です。
同じ構造のtypeでfooIdをキャストできるか?
/**
* Same Structures but type
*/
type FooIdType = {
_fooIdBrand: string; // To prevent type errors
}
type AnotherFooIdType = {
_fooIdBrand: string; // To prevent type errors
}
// Intersection
type FooFooIdType = FooIdType & {
_fooFooIdBrand: string; // Not _fooIdBrand
}
// more properties but not intersection
type FooBarIdType = {
_fooBarIdBrand: string;
_fooIdBrand: string;
_barIdBrand: string;
}
fooId as FooIdType; // ok. If same structure, type is also accepted
fooId as FooFooIdType; // error, Not extended but intersection
fooId as FooBarIdType; // error
同じ構造のタイプは、名前が違っても許容するみたいです。
また、typeには継承の概念がないのでインターセクションだけ試しましたが、そっちはやっぱりダメですね。そこからプロパティを増やしたタイプがダメなのもわかります。
ちなみにユニオンでもダメでした。
FooIdTypeの型アサーション
const fooIdType: FooIdType = "fooId" as any;
str = fooIdType as any; // ok
str = fooIdType as string; // error
fooIdType as AnotherFooIdType; // ok
fooIdType as FooFooIdType; // ok
fooIdType as FooBarIdType; // ok
文字列へのアサーションは同じ振る舞いですが、他の類似の型へのアサーションは通ってしまいました。
typeをinterfaceでアサーション
fooIdType as FooId; // ok
fooIdType as FooFooId; // ok
fooIdType as FooBarId; // ok
これも全部通ります
型アサーション対応表
FooId, FooIdTypeそれぞれに対する型アサーションの結果
oアサート受け入れ
x型アサーションエラー
-実験なし
| 条件 | Type | Interface | Type as Interface | Interface as Type |
|---|---|---|---|---|
| 基準 | - | - | o | o |
| サブタイプ | x | x | - | - |
| 別名 | o | o | - | - |
| 継承 | - | o | o | - |
| インターセクション | o | - | - | x |
| ユニオン | o | - | - | - |
| 拡張 | o | x | o | x |
まとめ
名前主義パターンを目指したものでしたが、受け入れてしまっているパターンもちらほらですね。
それでもインターフェイスはまだ拒否しているものが多く、名前主義っぽさがあります。
型アサーションの条件
anyなどを省くと、今回の実験ではこんな結果になりました。
- type
- 構造的なスーパータイプを全て受け入れます。
- interfaceも構造だけをみて受け入れます
- interface
- 構造的に全く同一なものを、type/interface問わず受け入れます。
- 明確に継承しているものも受け入れます。
こうしてみるとinterfaceは比較的堅牢な気もしてきますが、numberを受け入れてしまうEnumや綴りにspecificなリテラルタイプと適宜使い分ける必要がありそうです。