Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

neverが絡んだ型推論についての質問です #627

Closed
matoruru opened this issue Mar 4, 2023 · 5 comments
Closed

neverが絡んだ型推論についての質問です #627

matoruru opened this issue Mar 4, 2023 · 5 comments
Labels
type:読者の質問 本を読んで分からなかったこと、TypeScriptで分からないこと

Comments

@matoruru
Copy link

matoruru commented Mar 4, 2023

型の一致不一致を確認するための型Equalを作りました。

type Equal<A, B> = A extends B ? (B extends A ? "true" : "false") : "false"

Equalに渡す引数のABが等しければ"true"、そうでなければ"false"になるように作っています。

type Test1 = Equal<number, number>
// true

type Test2 = Equal<number, string>
// false

しかしneverを渡した時だけ挙動が変わってしまいます。

type Test3 = Equal<never, string>
// never

type Test4 = Equal<never, never>
// never

type Test5 = Equal<string, never>
// false

Equalの条件では等しくなければ"false"に推論されるはずですし、Test4ではneverneverの比較なので"true"に推論されるのが妥当だと考えていました。Test5だけ"false"に推論されているのも理解できません。

これはneverの仕様から説明できるでしょうか・・・?

ご回答お待ちしています。よろしくお願いいたします。

関連ページ: https://typescriptbook.jp/reference/statements/never

@suin suin added the type:読者の質問 本を読んで分からなかったこと、TypeScriptで分からないこと label Mar 4, 2023
@canalun
Copy link
Contributor

canalun commented Mar 4, 2023

自分はこのレポジトリの管理者ではないのですが、たまたまこのissueを見かけ、参考になりそうな情報を持っていたので一応共有いたします👶
参考になれば幸いです!

挙動の理由

前提として、T extends U ? X : Yという式(Conditional Types)のTneverを代入させると、その結果は例外なくXYの値とは無関係にneverになると言われています。
私自身もソースコードを直接追ってみたことはないのですが、下記リンク先の説明が自分にとっては分かりやすく納得しています。
https://scrapbox.io/mrsekut-p/T_%E3%81%8Cnever%E3%81%AE%E6%99%82%E3%81%AE%E3%80%81T_extends_.._%E3%81%AF%E3%80%81%E5%95%8F%E7%AD%94%E7%84%A1%E7%94%A8%E3%81%A7never%E3%81%AB%E3%81%AA%E3%82%8B#:~:text=%E3%81%AA%E3%81%9C%E3%81%8B%EF%BC%9F%0A%09union%E5%9E%8B,%E3%81%AA%E3%81%A3%E3%81%A6%E3%81%97%E3%81%BE%E3%81%86
(英語でも良ければ、異なる言い方のほぼ同じ説明がこちらにもありました😃: https://stackoverflow.com/a/65492934)

なお、この挙動は公式ドキュメントでは言及されていない(ように思える)のですが、公式のissuesでは過去に言及されているようです👶
microsoft/TypeScript#23182 (comment)

この前提を踏まえると、 @matoruru さんの仰る挙動も説明がつくのではと考えています。

type Test3 = Equal<never, string> // never
type Test4 = Equal<never, never> // never
これら2つは、neverがConditional Types(extendsの式)の第一項にくるので問答無用でneverになってしまうのかと考えられます。

一方で
type Test5 = Equal<string, never> // false
は、
string extends never ? (never extends string ? "true" : "false") : "false"
となりますが、これは最初のstring extends neverが偽と評価されてfalseに転ぶのではないかと思いました。

解決策のアイデア

解決策のアイデアとしては、第一項を配列型にしてしまう策があります(先ほど紹介したissuesの議論にも登場しているようです: https://github.com/microsoft/TypeScript/issues/23182#issuecomment-379091887)。
type Equal<A, B> = [A] extends [B] ? ([B] extends [A] ? "true" : "false") : "false"

これで実際に試してみると、下記のように期待される結果になりました!👶
スクリーンショット 2023-03-04 20 51 16

playgroundへのリンク: https://www.typescriptlang.org/play?#code/C4TwDgpgBAogjgVwIYBsA8BBANFAQgPigF4oBtDAXSggA9gIA7AEwGczcqB+KAClI+p1GrMpSjcARMABOCCBKgAuKBIBmqFvICUSlepSaJAKCOhIUACoQWwAIzFYiVGgYIAtgCMI0nK8-f8IyYIAGMUJGloEIB7BhsoehtbZSsk02s7IwB6LKgAPU4TM2hU4AAmB3hkdD8vHygbaQBLBgBzQOCwiKjY+MTylIyy9JthnPzC03ASjIBmSqcaiAA3bxxGlvag0PDIqBi44AS5wZtZkeBz8YKi6csMgBYF6pcVtagGN+kOne793qO-Qep2ADwuYOuk2K9xsAFZns4Nm1fF8fl09gc+hlYSDYRc8ZCgA

留意点

ただ、私自身あまりわかっていないのですが、TSで型の同一性を正確に評価するにはなんだかもう少し工夫が必要なようです……!
これは公式のissuesでも議論されたことがあり、結論としては下記リンク先の型計算で実現できるようなのです。
microsoft/TypeScript#27024 (comment)
リンク先の議論では、最初のほうでまさに @matoruru さんの提示されている方法とそれではカバーできないらしき部分が言及されています。
この魔法の式の仕組みはリンク先の議論でも言及されているようで、また、調べられている記事もちらほらある (e.g. https://zenn.dev/yumemi_inc/articles/ff981be751d26c) ようなのですが、あまりわかっておらずここについては特に自分からはこれ以上伝えられなさそうです……すみません!

@suin
Copy link
Contributor

suin commented Mar 4, 2023

これはconditional typeがunion distributionするのと、neverが空のユニオンであるために発生する事象です。

conditional typeの型パラメーターはユニオンに展開されます。

type X<T> = T extends string ? 'string' : 'not-string';

のような型があるとき、

type Y = X<string | number>;

type Y = X<string> | X<number>;

のように分配されます。

次のようにneverを渡した場合ですが、

type Y = X<never>;

neverは空のユニオンと決められているので、分配が行えなくなります。そのため、「分配できませんでした」の意味でneverが返されます。

本当にneverが空のユニオンかどうかは、次のように1つの型から構成されるユニオン型から、その最後の1つの型を取り除くことで確認することもできます。

type X = Exclude<string, string>; // stringからなるユニオンからstringを取り除いたら…
// X は neverになる

ちなみに、neverをconditonal typeで扱える方法もあります: https://zenn.dev/suin/scraps/8828d6c915298c

@suin
Copy link
Contributor

suin commented Mar 4, 2023

@canalun さんも回答ありがとうございます!

@matoruru
Copy link
Author

matoruru commented Mar 4, 2023

お二人ともご回答ありがとうございました!理解できました🐈

@canalunさん Playgroundのリンクまで・・・!たくさん情報元も教えてくださって、ご丁寧にありがとうございます😀

@suinさん

type X = Exclude<string, string>; // stringからなるユニオンからstringを取り除いたら…
// X は neverになる

ここが非常に直感的でわかりやすく、理解の助けになりました🐈

@matoruru matoruru closed this as completed Mar 4, 2023
@suin
Copy link
Contributor

suin commented Mar 6, 2023

理解の手助けになったようでなによりです😌

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type:読者の質問 本を読んで分からなかったこと、TypeScriptで分からないこと
Projects
None yet
Development

No branches or pull requests

3 participants