Oasis's Cloud

一个人的首要责任,就是要有雄心。雄心是一种高尚的激情,它可以采取多种合理的形式。
—— 《一个数学家的辩白》

类型安全

作者:oasis


反模式指的是当存在更好的替代方案时,却采用了常见的、低效的设计。基本类型偏执是一种反模式,它指的是用基础类型表示很多概念。例如用 number 类型表示邮编,用 string 表示电话号码等。

基本类型偏执的处理方法主要体现在对类型进行抽象。针对邮编来说:

function generateCode(code: Code) {}

function generateCode(code: number) {}

显然第一种函数签名更好,在代码提示和编译时,更容易校验错误。

Code 类型可以采用如下方式定义:

declare const CodeType: unique symbol
class Code {
    readonly value: number
    [CodeType]: void
    constructor(value: number) {
        this.value = value
    }
}

上述代码的关键点是 declare const CodeType: unique symbol[CodeType]: void,这样可以避免 TypeScript 将具有相同形状的类型解释为 Code 类型。

通过定义更具意义的 Code 类型,我们可以避免基本类型偏执的陷阱,但并没有对类型起到更严格的约束,因为 Code 类型关注的是类型与意义的关系。

类型除了具有意义,还需要进行范围的约束。例如邮编应该是不超过6位的数字。

解决约束问题可以通过构造函数进行处理:

declare const CodeType: unique symbol
class Code {
    readonly value: number
    [CodeType]: void
    constructor(value: number) {
        if(value >= 100000) throw new Error()
        this.value = value
    }
}

除了通过构造函数外,我们也可以通过 getter 方法进行约束。

一般来说构造函数不应该做很复杂的事情,在处理复杂的范围计算问题时,可以通过工厂函数来进行约束

declare const CodeType: unique symbol
class Code {
    readonly value: number
    [CodeType]: void
    constructor(value: number) {
        this.value = value
    }
    static makeCode(value: number): Code {
        if(value >= 100000) throw new Error()
        return new Code(value)
    }
}

何时使用类型转换

在联合类型中,可能会使用到类型转换,因为在这种情况下,我们知道的比编译器更多。

type Left = 'left'
type Right = 'right'

// 函数返回联合类型
function getDirection(angle: number): Left | Right | undefined {
    if (angle === 180) return 'left'
    if (angle === 0) return 'right'
    return undefined
}

// 使用类型转换
const result = getDirection(180)  // 类型是 Left | Right | undefined

if (result === 'left') {
    // 通过运行时检查后,使用类型断言进行类型转换
    const leftValue: Left = result as Left  // 类型转换:Left | Right | undefined → Left
    console.log(leftValue)  // 'left'
}

使用 unknown 强制类型转换

当两个类型形状完全不同,无法直接转换时,需要借助 unknown 作为中间类型。

unknown 是 TypeScript 的顶层类型,可以安全地转换为任何类型。通过 as unknown as TargetType 的方式,我们可以绕过 TypeScript 的类型检查,进行强制类型转换。

// API 返回的原始数据(字符串)
type ApiResponse = string

// 我们期望的数据结构(对象)
type UserData = {
    name: string
    age: number
}

function fetchUserData(): ApiResponse {
    return '{"name": "Alice", "age": 30}'  // 实际返回 JSON 字符串
}

// 更实际的用法:解析 JSON 字符串
function parseUserData(response: ApiResponse): UserData {
    const parsed = JSON.parse(response)  // parsed 的类型是 any
    // 使用 unknown 进行类型转换,确保类型安全
    return parsed as unknown as UserData
}

向上和向下类型转换

将子类型转为父类型是一种常见的转换操作,TypeScript 中可以隐式实现。

但父类型向子类型转换并不一定安全。

关联

any 和 unknown 和 never 和 void 的差异