热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

11个技巧让你成为更好的Typescript程序员原创

原标题:11

原标题:11个技巧让你成为更好的 Typescript 程序员
原创

学习 Typescript 通常是一次重新发现之旅。您的最初印象可能非常具有欺骗性:这不就是一种注释 Javascript 的方式,所以编译器可以帮助我找到潜在的错误吗?

通过 r/mevlix@reddit

虽然这句话通常是正确的,但随着您继续前进,您会发现该语言最不可思议的力量在于组合、推断和操纵类型。

本文将总结几个技巧,帮助您充分发挥该语言的潜力。

#1 思考{Set}

类型对于程序员来说是一个日常概念,但要简洁地定义它却出奇地困难。我发现将Set用作概念模型会很有帮助。

例如,新学习者发现 Typescript 的组合类型的方式违反直觉。举个很简单的例子:

type Measure = { radius: number };
type Style = { color: string };
// typed { radius: number; color: string }
type Circle = Measure & Style;




如果您&在逻辑 AND 的意义上解释运算符,您可能希望Circle它是一个虚拟类型,因为它是两种类型的结合,没有任何重叠字段。这不是打字稿的工作方式。相反,在Set中思考更容易推断出正确的行为:


  • 每种类型都是一值。

  • 有些集合是无限的:字符串、数字;一些有限的:布尔值,未定义的,......


  • unknown通用集(包括所有值),而never空集(包括无值)。

  • 类型Measure是所有对象的集合,包含一个名为 的数字字段radius。与 相同Style


  • &运算符创建一个IntersectionMeasure & Style表示包含radius和字段的一组对象color,这实际上是一个较小的 Set,但具有更常用的字段。

  • 类似地,|运算符创建一个Union:一个更大的 Set 但可能具有更少的常用字段(如果组合了两个对象类型)。

Set还有助于理解可赋值性:仅当值的类型是目标类型的子集时才允许赋值:

type ShapeKind = 'rect' | 'circle';
let foo: string = getSomeString();
let shape: ShapeKind = 'rect';
// disallowed because string is not subset of ShapeKind
shape = foo;
// allowed because ShapeKind is subset of string
foo = shape;




以下文章对 Set 中的思维方式进行了出色的详尽介绍。




TypeScript 和集合论 |伊万奥韦耶罗

集合论如何帮助理解 TypeScript 中的类型可分配性和解析度?


ivov.dev



#2 理解声明的类型和缩小的类型

一个极其强大的打字稿功能是基于控制流的自动类型缩小。这意味着变量在代码位置的任何特定点都有两种类型与之关联:声明类型和缩小类型。

function foo(x: string | number) {
if (typeof x === 'string') {
// x's type is narrowed to string, so .length is valid
console.log(x.length);
// assignment respects declaration type, not narrowed type
x = 1;
console.log(x.length); // disallowed because x is now number
} else {
...
}
}





#3 使用有区别的联合而不是可选字段

在定义一组像 Shape 这样的多态类型时,很容易从以下开始:

type Shape = {
kind: 'circle' | 'rect';
radius?: number;
width?: number;
height?: number;
}
function getArea(shape: Shape) {
return shape.kind === 'circle' ?
Math.PI * shape.radius! ** 2
: shape.width! * shape.height!;
}




非空断言(访问radiuswidth和字段时)是必需的,因为与其他字段height之间没有建立关系。kind相反,discriminated union 是一个更好的解决方案:

type Circle = { kind: 'circle'; radius: number };
type Rect = { kind: 'rect'; width: number; height: number };
type Shape = Circle | Rect;
function getArea(: Shape) {
return shape.kind === 'circle' ?
Math.PI * shape.radius ** 2
: shape.width * shape.height;
}




类型缩小消除了强制转换的需要。

#4 使用类型谓词来避免类型断言

如果你以正确的方式使用打字稿,你应该很少会发现自己使用显式类型断言(比如value as SomeType);但是,有时您仍然会感到冲动,例如:

type Circle = { kind: 'circle'; radius: number };
type Rect = { kind: 'rect'; width: number; height: number };
type Shape = Circle | Rect;
function isCircle(shape: Shape) {
return shape.kind === 'circle';
}
function isRect(shape: Shape) {
return shape.kind === 'rect';
}
const myShapes: Shape[] = getShapes();
// error because typescript doesn't know the filtering
// narrows typing
const circles: Circle[] = myShapes.filter(isCircle);
// you may be inclined to add an assertion:
// const circles = myShapes.filter(isCircle) as Circle[];




一个更优雅的解决方案是改为更改isCircleisRect返回类型谓词,这样它们可以帮助 Typescript 在调用后进一步缩小类型范围filter

function isCircle(shape: Shape): shape is Circle {
return shape.kind === 'circle';
}
function isRect(shape: Shape): shape is Rect {
return shape.kind === 'rect';
}
...
// now you get Circle[] type inferred correctly
const circles = myShapes.filter(isCircle);





#5 控制联合类型的分布方式

类型推断是 Typescript 的本能;大多数时候,它会默默地为你工作。但是,您可能需要对含糊不清的细微情况进行干预。分布式条件类型就是其中一种情况。

假设我们有一个ToArray辅助类型,如果输入类型还不是一个,它会返回一个数组类型:

type ToArray<T> = T extends Array<unknown> ? T: T[];




对于以下类型,您认为应该推断出什么?

type Foo = ToArray<string|number>;




答案是string[] | number[]。但这是模棱两可的。为什么不(string | number)[]呢?

默认情况下,当 typescript 遇到string | number泛型参数(T此处)的联合类型(此处)时,它会分配到每个成分中,这就是您得到string[] | number[].这种行为可以通过使用特殊语法并包装T在一对 中来改变[],例如:

type ToArray<T> = [T] extends [Array<unknown>] ? T : T[];
type Foo = ToArray<string | number>;




NowFoo被推断为 type(string | number)[]

#6 在编译时使用详尽检查来捕获未处理的情况

当对枚举进行 switch-casing 时,一个好习惯是主动为不期望的情况犯错,而不是像在其他编程语言中那样默默地忽略它们:

function getArea(shape: Shape) {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'rect':
return shape.width * shape.height;
default:
throw new Error文章来源地址4871.html('Unknown shape kind');
}
}




使用 Typescript,您可以让静态类型检查通过使用以下never类型更早地为您找到错误:

function getArea(shape: Shape) {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'rect':
return shape.width * shape.height;
default:
// you'll get a type-checking error below
// if any shape.kind is not handled above
const _exhaustiveCheck: never = shape;
throw new Error('Unknown shape kind');
}
}




有了这个,就不可能getArea在添加新的形状种类时忘记更新函数。

该技术背后的基本原理是该never类型不能分配任何东西,除了never.shape.kind如果case 语句穷尽了所有候选者,则唯一可能到达的类型default是 never;但是,如果任何候选人未被涵盖,它将泄漏到default分支并导致无效分配。

type#7更喜欢interface

在打字稿中,当用于键入对象时type,它们是非常相似的结构。尽管可能存在争议,但我的建议是在大多数情况下interface始终使用,并且仅在满足以下任一条件时才使用:typeinterface


  • 您想要利用 的“合并”功能interface


  • 您有涉及类/接口层次结构的 OO 样式代码。


否则,始终使用更通用的type构造会产生更一致的代码。

#8 在适当的时候优先使用元组而不是数组

对象类型是输入结构化数据的常www.yii666.com用方式,但有时您可能需要更简洁的表示方式,而是使用简单的数组。例如,我们Circle可以这样定义:

type Circle = (string | number)[];
const circle: Circle = ['circle', 1.0]; // [kind, radius]




但是这种输入不必要地松散,您可以通过创建类似['circle', '1.0'].我们可以通过使用 Tuple 来使其更严格:

type Circle = [string, number];
// you'll get an error below
const circle: Circle = ['circle', '1.0'];




元组用法的一个很好的例子是 React 的useState.

const [name, setName] = useState('');




它既紧凑又类型安全。

#9 控制推断类型的一般性或具体性

Typescript 在进行类型推断时使用合理的默认行为,旨在简化常见情况下的代码编写(因此类型不需要显式注释)。有几种方法可以调整它的行为。

  • 用于const缩小到最具体的类型

let foo = { name: 'foo' }; // typed: { name: string }
let Bar = { name: 'bar' } as const; // typed: { name: 'bar' }
let a = [1, 2]; // typed: number[]
let b = [1, 2] as const; // typed: [1, 2]
// typed { kind: 'circle; radius: number }
let circle = { kind: 'circle' as const, radius: 1.0 };
// the following won't work if circle wasn't initialized
// with the const keyword
let shape: { kind: 'circle' | 'rect' } = circle;





  • 用于satisfies检查类型而不影响推断类型

考虑以下示例:

type NamedCircle = {
radius: number;
name?: string;
};
const circle: NamedCircle = { radius: 1.0, name: 'yeah' };
// error because circle.name can be undefined
console.log(circle.name.length);




我们得到一个错误,因为根据circle的声明类型NamedCirclename字段确实可以是未定义的,即使变量初始值设定项提供了一个字符串值。当然,我们可以删除类型注释,但我们将放弃对对象: NamedCircle有效性的类型检查。circle相当进退两难。

幸运的是,Typescript 4.9 引入了一个新satisfies关键字,它允许您在不改变推断类型的情况下检查类型:

type NamedCircle = {
radius: number;
name?: string;
};
// error because radius violates NamedCircle
const wrongCircle = { radius: '1.0', name: 'ha' }
satisfies NamedCircle;
const circle = { radius: 1.0, name: 'yeah' }
satisfies NamedCircle;
// circle.name can't be undefined now
console.log(circle.name.length);




修改后的版本享有两个好处:保证对象文字符合NamedCircle类型,并且推断类型具有不可为空的name字段。

#10infer用于创建额外的泛型类型参数

在设计实用函数和类型时,您经常会觉得需要使用从给定类型参数中提取的类型。在这种情况下,infer关键字就派上用场了。它可以帮助您即时推断出新的类型参数。这里有两个简单的例子:

// gets the unwrapped type out of a Promise;
// idempotent if T is not Promise
type ResolvedPromise<T> = T extends Promise<infer U> ? U : T;
type t = ResolvedPromise<Promise<string>>; // t: string
// gets the flattened type of array T;
// idempotent if T is not array
type Flatten<T> = T extends Array<infer E> ? Flatten<E> : T;
type e = Flatten<number[][]>; // e: number




infer关键字的工作原理可以T extends Promise理解为:假设 T 与某些实例化的通用 Promise 类型兼容,即兴创作一个类型参数 U 使其工作。因此,如果T被实例化为Promise,则 的解U将是string

#11通过在类型操作上发挥创意来 保持DRY

Typescript 提供了强大的类型操作语法和一组非常有用的实用程序,可帮助您将代码重复减少到最低限度。这里只是一些特别的例子:

  • 而不是复制字段声明:

type User = {
age: number;
gender: string;
country: string;
city: string
};
type Demographic = { age: number: gender: string; };
type Geo = { country: string; city: string; };




,使用Pick实用程序提取新类型:

type User = {
age: number;
gender: string;
country: string;
city: string
};
type Demographic = Pick<User, 'age'www.yii666.comax-error-color)">|'gender'>;
type Geo = Pick<User, 'country'|'city'>;





  • 而不是复制函数的返回类型

function createCircle() {
return {
kind: 'circle' as const,
radius: 1.0
}
}
function transformCircle(circle: { kind: 'circle'; radius: number }) {
...
}
transformCircle(createCircle());




,用于ReturnType提取它:

function createCircle() {
return {
kind: 'circle' as const,
radius: 1.0
}
}
function transformCircle(circle: ReturnType<typeof createCircle>) {
...
}
transformCircle(createCircle());





  • 而不是并行同步两种类型的形状(此处为 typeof config 和 Factory):

type ContentTypes = 'news' | 'blog' | 'video';
// config for indicating what content types are enabled
const config = { news: true, blog: true, video: false }
satisfies Record<ContentTypes, boolean>;
// factory for creating contents
type Factory = {
createNews: () => Content;
createBlog: () => Content;
};




,使用Mapped Type和Template Literal Type根据配置的形状自动推断出正确的工厂类型:

type ContentTypes = 'news' | 'blog' | 'video';
// generic factory type with a inferred list of methods
// based on the shape of the given Config
type ContentFactory<Config extends Record<ContentTypes, boolean>> = {
[k in string & keyof Config as Config[k] extends true
? `create${Capitalize<k>}`
: never]: () => Content;
};
// config for indicating what content types are enabled
const config = { news: true, blog: true, video: false }
satisfies Record<ContentTypes, boolean>;
type Factory = ContentFactory<typeof config>;
// Factory: {
// createNews: () => Content;
// createBlog: () => Content;
// }




发挥您的想象力,您会发现无限的探索潜力。

包起来

这篇文章涵盖了 Typescript 语言中的一组相对高级的主题。实际上,您可能会发现直接应用它们并不常见;然而,此类技术被专门为 Typescript 设计的库大量使用:如Prisma和tRPC。了解这些技巧可以帮助您更好地了解这些工具如何在幕后发挥其魔力。


PS 我们正在构建ZenStack— 一个用于使用 Next.js + Typescript 构建安全 CRUD 应用程序的工具包。我们的目标是让您节省编写样板代码的时间,并专注于构建重要的东西——用户体验。

来源于:11个技巧让你成为更好的 Typescript 程序员
原创


推荐阅读
  • 前言小伙伴们大家好。从今天开始我们将从 ... [详细]
  • 使用eclipse创建一个Java项目的步骤
    本文介绍了使用eclipse创建一个Java项目的步骤,包括启动eclipse、选择New Project命令、在对话框中输入项目名称等。同时还介绍了Java Settings对话框中的一些选项,以及如何修改Java程序的输出目录。 ... [详细]
  • 枚举使用枚举我们可以定义一些带名字的常量。使用枚举可以清晰地表达意图或创建一组有区别的用例。TypeScript支持数字的和基于字符串的枚举。数字枚举首先我们看看数字枚举,如果你使 ... [详细]
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • 本文介绍了深入浅出Linux设备驱动编程的重要性,以及两种加载和删除Linux内核模块的方法。通过一个内核模块的例子,展示了模块的编译和加载过程,并讨论了模块对内核大小的控制。深入理解Linux设备驱动编程对于开发者来说非常重要。 ... [详细]
  • 预备知识可参考我整理的博客Windows编程之线程:https:www.cnblogs.comZhuSenlinp16662075.htmlWindows编程之线程同步:https ... [详细]
  • Java自带的观察者模式及实现方法详解
    本文介绍了Java自带的观察者模式,包括Observer和Observable对象的定义和使用方法。通过添加观察者和设置内部标志位,当被观察者中的事件发生变化时,通知观察者对象并执行相应的操作。实现观察者模式非常简单,只需继承Observable类和实现Observer接口即可。详情请参考Java官方api文档。 ... [详细]
  • 本文介绍了在wepy中运用小顺序页面受权的计划,包含了用户点击作废后的从新受权计划。 ... [详细]
  • EPPlus绘制刻度线的方法及示例代码
    本文介绍了使用EPPlus绘制刻度线的方法,并提供了示例代码。通过ExcelPackage类和List对象,可以实现在Excel中绘制刻度线的功能。具体的方法和示例代码在文章中进行了详细的介绍和演示。 ... [详细]
  • 接口我们使用接口来定义对象的类型。接口是对象的状态(属性)和行为(方法)的抽象(描述)简单理解就是:为我们的代码提供一种约定我们使用关键字interface来声 ... [详细]
  • java.lang.Class.getDeclaredMethod()方法java.lang.Class.getDeclaredMethod()方法用法实例教程-方法返回一个Met ... [详细]
  • JavaScript实现拖动对话框效果
    原标题:JavaScript实现拖动对话框效果代码实现:<!DOCTYPEhtml><htmllan ... [详细]
  • node.js 全局变量说明
    原标题:node.js全局变量说明文章目录全局对象 ... [详细]
author-avatar
GIfi炬辉_904
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有