在JavaScript本身,有很多方法来编写函数。再加上TypeScript,突然间就有很多东西需要考虑了。所以在一些朋友的帮助下,我把你通常需要/遇到的各种函数形式和简单的例子放在一起。
请记住,不同的语法有大量的组合。我只包括那些不太明显的组合或在某些方面独特的组合。
首先,我对事物的语法最大的困惑是把返回类型放在哪里。我什么时候使用: ,什么时候使用=> 。这里有几个快速的例子,如果你把这个帖子作为一个快速参考,可能会帮助你加速:
// Simple type for a function, use =>
type FnType = (arg: ArgType) => ReturnType
// Every other time, use :
type FnAsObjType = {
(arg: ArgType): ReturnType
}
interface InterfaceWithFn {
fn(arg: ArgType): ReturnType
}
const fnImplementation = (arg: ArgType): ReturnType => {
/* implementation */
}
我想这是让我感到困惑的最大原因。写完这篇文章后,现在我知道,我唯一使用=> ReturnType 的时候是我把一个函数类型定义为一个类型本身。其他时候,请使用: ReturnType 。
继续读下去,看看在典型的代码例子中,这一点是如何发挥的。
函数声明
// inferred return type
function sum(a: number, b: number) {
return a + b
}
// defined return type
function sum(a: number, b: number): number {
return a + b
}
在下面的例子中,我们将使用明确的返回类型,但从技术上讲,你不需要指定这个。
函数表达式
// named function expression
const sum = function sum(a: number, b: number): number {
return a + b
}
// annonymous function expression
const sum = function (a: number, b: number): number {
return a + b
}
// arrow function
const sum = (a: number, b: number): number => {
return a + b
}
// implicit return
const sum = (a: number, b: number): number => a + b
// implicit return of an object requires parentheses to disambiguate the curly braces
const sum = (a: number, b: number): {result: number} => ({result: a + b})
你也可以在变量旁边添加一个类型注释,然后函数本身就会承担这些类型:
const sum: (a: number, b: number) => number = (a, b) => a + b
而你可以提取该类型:
type MathFn = (a: number, b: number) => number
const sum: MathFn = (a, b) => a + b
或者你可以使用对象类型的语法:
type MathFn = {
(a: number, b: number): number
}
const sum: MathFn = (a, b) => a + b
如果你想给函数添加一个类型化的属性,这可能很有用。
type MathFn = {
(a: number, b: number): number
operator: string
}
const sum: MathFn = (a, b) => a + b
sum.operator = '+'
这也可以用一个接口来完成:
interface MathFn {
(a: number, b: number): number
operator: string
}
const sum: MathFn = (a, b) => a + b
sum.operator = '+'
然后还有declare function 和declare namespace ,其目的是为了说。"嘿,存在一个具有这个名称和类型的变量"。我们可以用它来创建类型,然后用typeof 把这个类型分配给我们的函数。你经常会发现declare 用在.d.ts 文件中,为图书馆声明类型:
declare function MathFn(a: number, b: number): number
declare namespace MathFn {
let operator: '+'
}
const sum: typeof MathFn = (a, b) => a + b
sum.operator = '+'
如果在type 、interface 和declare function 之间选择,我想我个人更喜欢type ,除非我需要interface提供的扩展性。只有当我真的想告诉编译器一些它还不知道的东西(比如一个库)时,我才会真正使用declare 。
可选/默认参数
可选参数:
const sum = (a: number, b?: number): number => a + (b ?? 0)
注意,这里的顺序很重要。如果你让一个参数成为可选参数,那么后面的所有参数也需要是可选的。这是因为有可能调用sum(1) ,但不可能调用sum(, 2) 。然而,可以调用sum(undefined, 2) ,如果这是你想启用的,那么你也可以这样做:
const sum = (a: number | undefined, b: number): number => (a ?? 0) + b
默认参数
当我写这篇文章时,我认为使用默认参数而不使该参数可选是没有用的,但事实证明,当你有一个默认值时,TypeScript会把它当作一个可选参数。所以这就可以了:
const sum = (a: number, b: number = 0): number => a + b
sum(1) // results in 1
sum(2, undefined) // results in 2
所以这个例子在功能上等同于。
const sum = (a: number, b: number | undefined = 0): number => a + b
TIL。
有趣的是,这也意味着,如果你想让第一个参数是可选的,但第二个参数是必须的,你可以不使用| undefined 。
const sum = (a: number = 0, b: number): number => a + b
sum(undefined, 3) // results in 3
然而,当你提取类型时,你需要手动添加| undefined,因为= 0 是一个JavaScript表达式,不是一个类型。
type MathFn = (a: number | undefined, b: number) => number
const sum: MathFn = (a = 0, b) => a + b
休息参数
Rest params是一个JavaScript功能,它允许你将函数调用的 "其余 "参数收集到一个数组中。你可以在任何参数位置(第一、第二、第三等等)使用它们。唯一的要求是,它是最后一个参数。
const sum = (a: number = 0, ...rest: Array<number>): number => {
return rest.reduce((acc, n) => acc + n, a)
}
而且你可以提取类型:
type MathFn = (a?: number, ...rest: Array<number>) => number
const sum: MathFn = (a = 0, ...rest) => rest.reduce((acc, n) => acc + n, a)
对象属性和方法
对象方法:
const math = {
sum(a: number, b: number): number {
return a + b
},
}
作为函数表达式的属性:
const math = {
sum: function sum(a: number, b: number): number {
return a + b
},
}
作为箭头函数表达式的属性(有隐式返回):
const math = {
sum: (a: number, b: number): number => a + b,
}
不幸的是,为了提取类型,你不能键入函数本身,你必须键入包围的对象。当函数被定义在对象字面中时,你不能用类型来注解函数本身。
type MathFn = (a: number, b: number) => number
const math: {sum: MathFn} = {
sum: (a, b) => a + b,
}
此外,如果你想在它上面添加一个属性,就像上面的一些例子一样,这在对象字面中是不可能做到的。你必须完全提取函数的定义:
type MathFn = {
(a: number, b: number): number
operator: string
}
const sum: MathFn = (a, b) => a + b
sum.operator = '+'
const math = {sum}
你可能已经注意到,这个例子和上面的一个例子完全一样,只是增加了const math = {sum} 。所以,是的,没有办法在对象声明中做所有这些事情。
类
类本身就是函数,但它们很特别(必须用new ),但本节将讨论如何在类体内定义函数。
这里是一个普通的方法,是类主体中函数的最常见形式。
class MathUtils {
sum(a: number, b: number): number {
return a + b
}
}
const math = new MathUtils()
math.sum(1, 2)
如果你希望函数被绑定到类的特定实例上,你也可以使用一个类的字段。
class MathUtils {
sum = (a: number, b: number): number => {
return a + b
}
}
// doing things this way this allows you to do this:
const math = new MathUtils()
const sum = math.sum
sum(1, 2)
// but it also comes at a cost that offsets any perf gains you get
// by going with a class over a regular object factor so...
然后,你可以提取这些类型。下面是第一个例子中的方法版本的样子。
interface MathUtilsInterface {
sum(a: number, b: number): number
}
class MathUtils implements MathUtilsInterface {
sum(a: number, b: number): number {
return a + b
}
}
有趣的是,看起来你仍然需要为函数定义类型,尽管这些是它应该实现的接口的一部分🤔 🤷♂️
最后一个说明。在TypeScript中,你还可以得到public ,private ,和protected 。我个人并不经常使用类,而且我不喜欢使用TypeScript的这些特殊功能。JavaScript很快就会为private 成员提供特殊的语法,这很整洁(了解更多)。
模块
导入和导出函数定义的方式与其他任何东西相同。TypeScript的独特之处在于,如果你想写一个带有模块声明的.d.ts 文件。让我们以我们的sum 函数为例。
const sum = (a: number, b: number): number => a + b
sum.operator = '+'
这里是我们要做的,假设我们把它导出为默认值。
declare const sum: {
(a: number, b: number): number
operator: string
}
export default sum
而如果我们希望它是一个命名的导出。
declare const sum: {
(a: number, b: number): number
operator: string
}
export {sum}
重载
我特别写过这方面的文章,你可以阅读:用TypeScript定义函数重载类型。 下面是那篇文章的例子。
type asyncSumCb = (result: number) => void
// define all valid function signatures
function asyncSum(a: number, b: number): Promise<number>
function asyncSum(a: number, b: number, cb: asyncSumCb): void
// define the actual implementation
// notice cb is optional
// also notice that the return type is inferred, but it could be specified
// as `void | Promise<number>`
function asyncSum(a: number, b: number, cb?: asyncSumCb) {
const result = a + b
if (cb) return cb(result)
else return Promise.resolve(result)
}
基本上你所做的是多次定义函数,只在最后一次真正实现它。重要的是,实现的类型要支持所有的重载类型,这就是为什么上面的cb 是可选的`。
生成器
我还没有在生产代码中使用过一次生成器......但是当我在TypeScript的操场上玩了一会儿,对于简单的情况来说,并没有什么好的方法。
function* generator(start: number) {
yield start + 1
yield start + 2
}
var iterator = generator(0)
console.log(iterator.next()) // { value: 1, done: false }
console.log(iterator.next()) // { value: 2, done: false }
console.log(iterator.next()) // { value: undefined, done: true }
TypeScript正确地推断出iterator.next() ,返回一个具有以下类型的对象。
type IteratorNextType = {
value: number | void
done: boolean
}
如果你想让yield 表达式完成值的类型安全,请给你分配给它的变量添加一个类型注释。
function* generator(start: number) {
const newStart: number = yield start + 1
yield newStart + 2
}
var iterator = generator(0)
console.log(iterator.next()) // { value: 1, done: false }
console.log(iterator.next(3)) // { value: 5, done: false }
console.log(iterator.next()) // { value: undefined, done: true }
现在,如果你试图调用iterator.next('3') ,而不是iterator.next(3) ,你会得到一个编译错误 🎉
异步
async/await 函数在TypeScript中的工作方式与在JavaScript中的工作方式完全相同,唯一不同的是,它们的返回类型总是一个 通用。Promise
const sum = async (a: number, b: number): Promise<number> => a + b
async function sum(a: number, b: number): Promise<number> {
return a + b
}
泛型
用一个函数声明。
function arrayify2<Type>(a: Type): Array<Type> {
return [a]
}
不幸的是,对于一个箭头函数(当TypeScript被配置为JSX时),函数的开头< ,对编译器来说是模糊的。"这是通用语法吗?还是JSX?"所以你必须添加一点东西来帮助它消除歧义。我认为最直接的做法是让它extends unknown 。
const arrayify = <Type extends unknown>(a: Type): Array<Type> => [a]
这很方便地给我们展示了泛型中extends 的语法,所以你可以这样做。
类型守卫
类型守卫是一种进行类型缩小的机制。例如,它允许你将string | number 的东西缩小到string 或number 。有一些内置的机制(如typeof x === 'string' ),但你也可以自己制作。这是我最喜欢的方法之一(向最初向我展示这个方法的朋友Peter致敬)。
你有一个数组,里面有一些虚假的值,你想让这些值消失。
// Array<number | undefined>
const arrayWithFalsyValues = [1, undefined, 0, 2]
在普通的JavaScript中,你可以这样做。
// Array<number | undefined>
const arrayWithoutFalsyValues = arrayWithFalsyValues.filter(Boolean)
不幸的是,TypeScript不认为这是一个类型缩小的保护,所以类型仍然是Array<number | undefined> (没有应用缩小)。
因此,我们可以编写我们自己的函数,并告诉编译器,它对给定的参数是否是一个特定的类型返回真/假。对我们来说,我们会说,如果给定参数的类型不包括在虚假的值类型中,我们的函数就返回真。
type FalsyType = false | null | undefined | '' | 0
function typedBoolean<ValueType>(
value: ValueType,
): value is Exclude<ValueType, FalsyType> {
return Boolean(value)
}
这样我们就可以做到这一点了。
// Array<number>
const arrayWithoutFalsyValues = arrayWithFalsyValues.filter(typedBoolean)
Woo!
断言函数
你知道有时你会做运行时检查以确保某些事情吗? 比如,当一个对象可以有一个属性的值或null ,你想检查它是否是null ,如果是null ,也许会抛出一个错误。 这里是你如何做这样的事情。
type User = {
name: string
displayName: string | null
}
function logUserDisplayNameUpper(user: User) {
if (!user.displayName) throw new Error('Oh no, user has no displayName')
console.log(user.displayName.toUpperCase())
}
TypeScript对user.displayName.toUpperCase() ,因为if语句是一个它所理解的类型保护。现在,假设你想把这个if 检查放在一个函数中。
type User = {
name: string
displayName: string | null
}
function assertDisplayName(user: User) {
if (!user.displayName) throw new Error('Oh no, user has no displayName')
}
function logUserDisplayName(user: User) {
assertDisplayName(user)
console.log(user.displayName.toUpperCase())
}
现在,TypeScript不再高兴了,因为对assertDisplayName 的调用不是一个充分的类型保护。我认为这是TypeScript的一个限制。 嘿,没有技术是完美的。无论如何,我们可以通过告诉TypeScript我们的函数做了一个断言来帮助它。
type User = {
name: string
displayName: string | null
}
function assertDisplayName(
user: User,
): asserts user is User & {displayName: string} {
if (!user.displayName) throw new Error('Oh no, user has no displayName')
}
function logUserDisplayName(user: User) {
assertDisplayName(user)
console.log(user.displayName.toUpperCase())
}
这也是将我们的函数变成类型缩小函数的另一种方法
结论
这绝对不是全部,但这是我发现自己在TypeScript中处理函数时写的很多常见语法。我希望这对你有帮助!把它加入书签并与你的朋友分享 😘