TypeScript를 사용한 형식화 프로그래밍 소개

유형이 지정된 언어로 유형을 작성하는 방법을 배우고 기존 JavaScript 지식으로 TypeScript를 더 빨리 마스터하십시오.

유형 자체는 복잡한 언어입니다.

저는 typescript가 JavaScript 위에 유형 주석을 추가하는 것이라고 생각했습니다. 이런 마음가짐으로 올바른 유형을 작성하는 것이 어떤 식으로든 실제 응용 프로그램을 구축하는 데 방해가 되고 종종 사용하게 될 정도로 까다롭고 벅차다는 것을 알게 됩니다 any. 그리고 그것 any으로 나는 모든 유형 안전성을 잃습니다.

실제로 유형을 사용하여 유형을 매우 복잡하게 만들 수 있습니다. 잠시 동안 typescript를 사용하고 나면 typescript 언어가 실제로 두 개의 하위 언어로 구성되어 있음을 느낍니다. 하나는 JavaScript이고 다른 하나는 입력된 언어입니다.

  • JavaScript 언어의 경우 세계는 JavaScript 값으로 구성됩니다.
  • 유형이 지정된 언어의 경우 세계는 유형으로 구성됩니다.

우리가 타이프스크립트 코드를 작성할 때 우리는 두 세계 사이를 왔다갔다 하면서 계속 춤을 춥니다: 우리는 타입 세계에서 타입을 만들고, 타입 어노테이션을 사용합니다(또는 컴파일러에 의해 암시적으로 유추되도록 합니다). 다른 방향으로 이동하십시오. JavaScript 변수에 typescript 키워드를 사용 하고 해당 유형을 검색하는 속성( 런타임에 유형을 확인하기 위해 JavaScript에서 제공 typeof하는 키워드를 참조하지 않음 ).typeof

이미지.png

JavaScript는 표현력이 풍부한 언어이며 유형이 지정된 언어도 마찬가지입니다. 사실 표현형 언어는 튜링 완전체로 나타났다.

나는 여기서 튜링의 완성도가 좋은지 나쁜지에 대해 어떠한 가치판단도 하지 않고, 그것이 설계에 의한 것인지 우연인지도 모릅니다(사실 튜링의 완성도가 우연히 달성되는 경우가 많습니다). 내 요점은 유형이 지정된 언어 자체가 겉보기에는 무해하지만 확실히 강력하고 성능이 뛰어나고 컴파일 시간에 임의의 계산이 가능하다는 것입니다.

TypeScript의 유형화 언어를 본격적인 프로그래밍 언어로 생각하기 시작했을 때 함수형 프로그래밍 언어의 특성 중 일부가 있다는 것을 깨달았습니다.

  • 반복 대신 재귀 사용
    • typescript 4.5에서는 꼬리 호출로 재귀를 최적화할 수 있습니다(어느 정도).
  • 유형(데이터)은 변경할 수 없습니다.

이 기사에서는 기존 JavaScript 지식을 활용하여 타이프스크립트를 더 빨리 마스터할 수 있도록 JavaScript와 대조하여 typescript의 유형 언어를 학습합니다.

변수 선언

JavaScript에서 세계는 JavaScript 값으로 구성되며 키워드 var, let를 사용 const하여 값을 참조하는 변수를 선언합니다. 예를 들어:

const obj = { name: 'foo' }
复制代码

유형이 지정된 언어에서 세계는 유형으로 구성되며 키워드 typeinterface유형 변수를 선언하는 데 사용합니다. 예를 들어:

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的数组mapfilter方法。

在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있는지 확인하기 위해 키워드를 사용해야 합니다.ArrlengthN
    • (기본 조건) 이 충족되면 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를 사용하는 것처럼유형 챌린지유형 프로그래밍 기술을 연습하십시오.

끝 맺는 말

이 기사에서 많은 것이 논의되었습니다. 이 게시물의 요점은 타이프스크립트를 가르치는 것이 아니라 타이프스크립트를 배우기 시작한 이후로 간과했을 수 있는 "숨겨진" 유형 언어를 다시 소개하는 것입니다.

Typed programming은 typescript 커뮤니티에서 틈새 시장이고 논의가 부족한 주제이며, 나는 그것에 대해 아무런 문제가 없다고 생각합니다. 왜냐하면 결국 유형을 추가하는 것은 목적을 위한 수단일 뿐입니다. 자바스크립트 애플리케이션. 그래서 나에게는 사람들이 JavaScript나 다른 프로그래밍 언어를 사용하는 것만큼 자주 유형 언어를 "진지하게" 공부하는 데 시간을 들이지 않는다는 것을 완전히 이해할 수 있습니다.

원래의

www.zhenghao.io/posts/type-…

추천

출처juejin.im/post/7079305963131371550