0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

構造が同じでも違う型に判定してよ!:TypeScript で Nominal Typing

Last updated at Posted at 2021-09-05

はじめに

前回の 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プロパティの存在が重要そうですが、他にいくつかの質問が浮かびます。

  1. typeだとどうなる?
  2. extendsは?
  3. プロパティ名同じで違う型は?

と言うわけで全部やってみました!

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なリテラルタイプと適宜使い分ける必要がありそうです。

0
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?