型付き言語で型を記述し、既存のJavaScriptの知識を使用してTypeScriptをより速く習得する方法を学びます。
タイプ自体は複雑な言語です
私は以前、typescriptはJavaScriptの上に型注釈を追加するだけだと思っていました。この考え方では、正しいタイプを書くことは、ある意味で実際のアプリケーションを構築することを妨げ、しばしばそれらを使用するように導くという点で、トリッキーで困難なものであることに気付くことがよくありますany
。そしてそれでany
、私はすべてのタイプの安全性を失います。
実際、型を使用して型を非常に複雑にすることができます。しばらくtypescriptを使用した後、typescript言語は実際には2つのサブ言語で構成されているように感じます-1つはJavaScriptで、もう1つは型付き言語です:
- JavaScript言語の場合、世界はJavaScript値で構成されます
- 型付き言語の場合、世界は型で構成されています
タイプスクリプトコードを書くとき、私たちは2つの世界の間を行き来し続けます。タイプの世界で型を作成し、JavaScriptの世界で「呼び出される」型注釈を使用します(またはコンパイラによって暗黙的に推測されます)。別の方向に進みます。JavaScripttypeof
変数でtypescriptキーワードを使用し、プロパティを使用して対応するタイプを取得します(これはtypeof
、実行時にタイプをチェックするためにJavaScriptによって提供されるキーワードを参照していません)。
JavaScriptは非常に表現力豊かな言語であり、型付き言語も同様です。実際、表現力豊かなタイプの言語はチューリング完全であることが示されています。
ここでは、チューリング完全性が良いか悪いかについて価値判断をしていません。それが設計によるものなのか偶然によるものなのかはわかりません(実際、チューリング完全性は偶然に達成されることがよくあります)。私のポイントは、型付き言語自体は、一見無害に見えますが、確かに強力でパフォーマンスが高く、コンパイル時に任意の計算ができるということです。
TypeScriptの型付き言語を本格的なプログラミング言語と考え始めたとき、関数型プログラミング言語の特徴さえあることに気づきました。
- 反復の代わりに再帰を使用する
- typescript 4.5では、末尾呼び出しを使用して再帰を最適化できます(ある程度)
- タイプ(データ)は不変です
この記事では、タイプスクリプトのタイプ言語をJavaScriptと対比して学習します。これにより、既存のJavaScriptの知識を活用してタイプスクリプトをより速く習得できます。
変数宣言
JavaScriptでは、世界はJavaScript値で構成されており、キーワード、、を使用しvar
てlet
、const
値を参照する変数を宣言します。例えば:
const obj = { name: 'foo' }
复制代码
型付き言語では、世界は型で構成されており、キーワードtype
とinterface
型変数を宣言するために使用します。例えば:
type Obj = { name: string }
复制代码
注:「型変数」の場合、より正確な名前は型同義語または型エイリアスになります。「型変数」という用語を使用して、JavaScript変数が値を参照する方法を類推します。
これは完全なアナロジーではありませんが、タイプエイリアスは新しいタイプを作成または導入しません。これらは既存のタイプの新しい名前にすぎません。しかし、このアナロジーによって、型付き言語の概念を簡単に説明できるようになることを願っています。
タイプと値は非常に関連しています。可能な値のコレクションを表すことをコアとするタイプ、およびそれらの値に対して実行できる有効な操作。このセットが有限である場合もあります。たとえばtype Name = 'foo' | 'bar'
、場合によっては、セットが無限である場合がありtype Age = number
ます。TypeScriptでは、型と値を統合し、それらを連携させて、実行時の値がコンパイル時の型と一致するようにします。
ローカル変数宣言
型付き言語で型付き変数を作成する方法について説明しました。ただし、型変数にはデフォルトでグローバルスコープがあります。ローカル型変数を作成するinfer
には、型言語でキーワードを使用できます。
type A = 'foo'; // global scope
type B = A extends infer C ? (
C extends 'foo' ? true : false // 只有在这个表达式中,C代表A
) : never;
复制代码
スコープ変数を作成するこの特定の方法は、JavaScript開発者には奇妙に思えるかもしれませんが、実際には、純粋に関数型プログラミング言語にそのルーツがあります。たとえば、Haskellでは、let
キーワードバインディングを使用して、次in
のようにスコープ内で代入を実行できlet {assignments} in {expression}
ます。
let two = 2; three = 3 in two * three
// ↑ ↑
// two and three are only in scope for the expression `two * three`
复制代码
等価性の比較と条件付き分岐
在JavaScript中,我们可以使用===
、==
和if语句或者条件(三元)运算符?
来执行相等校验和条件分支。
另一方面,在类型语言中,我们使用extends
关键字进行“相等检查”,并且条件(三元)运算符?
的使用也适用于条件分支:
TypeC = TypeA extends TypeB ? TrueExpression : FalseExpression
复制代码
如果TypeA
是可分配给TypeB
或者可替代TypeB
的,那么我们进入第一个分支,从TrueExpression
中获得类型并分配给TypeC
;否则我们从FalseExpression
中获得类型作为TypeC
的结果。
JavaScript中的一个具体例子:
const username = 'foo'
let matched
if (username === 'foo') {
matched = true
} else {
matched = false
}
复制代码
将其翻译为类型语言:
type Username = 'foo'
type Matched = Username extends 'foo' ? true : false
复制代码
extends
关键字是多功能的。它也可以对通用类型参数应用约束。例如:
function getUserName<T extends {name: string}>(user: T) {
return user.name
}
复制代码
通过添加通用约束,T extends { name: string }
,我们确保我们的函数参数总是由字符串类型的name
属性组成。
通过对对象类型的索引来检索属性的类型
在JavaScript中,我们可以用方括号来访问对象属性,例如obj['prop']
或者点操作符,例如obj.prop
。
在类型语言中,我们也可以用方括号提取属性类型。
type User = { name: string; age: number; }
type Name = User['name']
复制代码
这不仅适用于对象类型,我们还可以用元组和数组来索引类型。
type Names = string[]
type Name = Names[number]
type Tupple = [string, number]
type Age = Tupple[1]
type Info = Tupple[number]
复制代码
Functions
函数是任何JavaScript程序中主要的可重复使用的“构建块”。它们接收一些输入(some JavaScript values)并返回一个输出(也是some JavaScript values)。在类型语言中,我们有泛型。泛型将类型参数化,就像函数把值参数化一样。因而,泛型在概念上类似于JavaScript中的函数。
比如,在JavaScript中:
function fn (a, b = 'world') {
return [a, b]
}
const result = fn('hello') // ['hello', 'world']
复制代码
对于类型语言,可以这么做:
type Fn<A extends string, B extends string = 'world'> = [A, B]
// ↑ ↑ ↑ ↑ ↑
// name parameter parameter type default value function body/return statement
type Result = Fn<'hello'> // ['hello', 'world']
复制代码
但是这仍然不是一个完美的比喻:泛型绝对不是和JavaScript中的函数完全一样。比如有一点,与JavaScript中的函数不同的是,泛型不是类型语言中的一等公民。这意味着我们不能像将函数传给另一个函数那样,将一个泛型传给另一个泛型,因为typescript不允许泛型作为类型参数。
Map和filter
在类型语言中,类型是不可改变的。如果我们想改变一个类型的某个部分,我们必须将现有的类型转成新的类型。在类型语言中,数据结构(即对象类型)遍历细节和均匀地应用转换由映射类型抽象出来。我们可以用它实现概念上类似于JavaScript的数组map
和filter
方法。
在JavaScript中,假设我们想把一个对象的属性从数字转换为字符串。
const user = {
name: 'foo',
age: 28,
};
function stringifyProp (object) {
return Object.fromEntries(
Object.entries(object)
.map(([key, value]) => [key, String(value)])
)
}
const userWithStringProps = stringifyProp(user);
复制代码
在类型语言中,映射是用这种语法[k in keyof T]
完成的,其中keyof
操作符拿到的是属性名的一个字符串联合类型。
type User = {
name: string
age: number
}
type StringifyProp<T> = {
[K in keyof T]: string
}
type UserWithStringProps = StringifyProp<User> // { name: string; age: string; }
复制代码
在JavaScript中,我们可以基于一些标记来过滤掉一个对象的属性。例如,我们可以过滤掉所有非字符串类型的属性。
const user = {
name: 'foo',
age: 28,
};
function filterNonStringProp (object) {
return Object.fromEntries(
Object.entires(object)
.filter([key, value] => typeof value === 'string')
)
}
const filteredUser = filterNonStringProp(user) // { name: 'foo' }
复制代码
在类型语言中,还可以通过as
操作符和never
类型:
type User = {
name: string;
age: number;
};
type FilterNonStringProp<T> = {
[K in keyof T as T[K] extends string ? K : never]: string;
};
type FilteredUser = FilterNonStringProp<User>;
复制代码
在typescript中,有一堆内置工具类型(泛型)用于转换类型,所以很多时候你不必重复造轮子。
模式匹配
我们还可以用infer
关键字在类型语言中进行模式匹配。
例如,在JavaScript应用程序中,我们可以使用正则来提取字符串的某一部分:
const str = 'foo-bar'.replace(/foo-*/, '')
console.log(str) // 'bar'
复制代码
在类型语言中等价于:
type Str = 'foo-bar'
type Bar = Str extends `foo-${infer Rest}` ? Rest : never // 'bar'
复制代码
递归,而不是迭代
就像许多纯函数式编程语言一样,在类型语言中,没有for循环的语法结构来迭代一个数据列表。递归代替了循环的位置。
比方说,在JavaScript中,我们想写一个函数来返回一个数组,其中同一个项重复多次。下面是某种实现方法:
function fillArray(item, n) {
const res = [];
for(let i = 0; i < n; i++) {
res[i] = item;
}
return res;
}
复制代码
递归的写法是:
function fillArray(item, n, arr = []) {
return arr.length === n ? arr : filleArray(item, n, [item, ...arr]);
}
复制代码
我们如何在类型语言中写出这个等价关系?下面是如何得出一个解决方案的逻辑步骤:
FillArray
と呼ばれるジェネリックを 作成します(ジェネリックは型付き言語の関数のようなものだと言ったのを覚えていますか?)FillArray<Item, N extends number, Arr extends Item[] = []>
- 「関数本体」では、
extends
キーワードを使用してArr
、length
プロパティがすでに存在するかどうかを確認する必要がありますN
- それが満たされている場合
N
(基本条件)、単純に戻りますArr
- 到達していない場合は
N
、繰り返して1つ追加Item
しArr
ます。
- それが満たされている場合
これをまとめると、次のようになります。
type FillArray<Item, N extends number, Arr extends Item[] = []> =
Arr['length'] extends N ? Arr : FillArray<Item, N, [...Arr, Item]>;
type Foos = FillArray<'foo', 3> // ['foo', 'foo', 'foo']
复制代码
再帰深度の上限
TypeScript 4.5以前は、最大再帰深度は45でした。TypeScript 4.5では、末尾呼び出しの最適化があり、上限が999に引き上げられています。
プロダクションコードでタイプ体操を避ける
タイププログラミングは、通常のアプリケーションで必要とされるよりもはるかに複雑な、非常に複雑なベルやホイッスルになると、冗談めかして「タイプ体操」と呼ばれることがあります。例えば:
- 中国のチェスをシミュレートする
- シミュレートされた三目並べゲーム
- 算術を実装する
これらはより学術的な演習であり、次の理由で本番アプリケーションには適していません。
- 理解するのが難しい、特に難解なタイプスクリプト機能
- コンパイラのエラーメッセージが長すぎてわかりにくいため、デバッグが困難です
- コンパイルが遅い
LeetCodeにコアプログラミングスキルを練習させるのと同じように、タイプチャレンジタイププログラミングスキルを練習してください。
結びの言葉
この記事では多くのことが議論されています。この投稿のポイントは、Typescriptを教えることではなく、Typescriptを学び始めてから見落としていたかもしれない「隠された」タイプの言語を再紹介することです。
型付きプログラミングは、typescriptコミュニティではニッチで議論の余地のあるトピックであり、これに問題はないと思います。結局、型を追加することは、より信頼性の高いWebを作成するための手段にすぎないからです。 JavaScriptアプリケーション。だから私には、JavaScriptや他のプログラミング言語のように人々がタイプされた言語を「真剣に」研究するのに時間がかからないことは完全に理解できます。