【问题标题】:Typescript function which casts string value based on predefined datatype for particular keysTypescript 函数,它根据特定键的预定义数据类型转换字符串值
【发布时间】:2021-07-27 02:46:04
【问题描述】:

编辑:我已经更新了问题底部的工作代码。


我有一个名为get 的函数,它接受key 作为参数并返回相应的 价值。
此函数为每个key 维护value始终是字符串)和datatype
喜欢:

    key         value(stored as string)     dataType
    -------------------------------------------------
    count       '10'                        number
    name        'foo'                       string
    isHoliday   '1'                         boolean
    ===== more rows here =====

在返回值之前,它会转换为指定的数据类型。

例如:

  • 当key为count时,其值'10'被转换为数据类型number,值为10
  • 当key为isHoliday时,其值'1'被转换为数据类型boolean,值为true

get 函数以正确的类型返回正确的值。
但是 typescript 在名为 place.const count: number = get('count')
并产生类型错误,如:Type 'string | number | boolean' is not assignable to type 'number'.

代码:


type TypeName = 'number' | 'string' | 'boolean'
type ItemKey = 'count' | 'name' | 'isHoliday'

function get(key: ItemKey) {
    
    /**
     * Predefined data
     */
    const data: {
        [key: string]: {
            value: string
            dataType: TypeName
        }
    } = {
        count       : { value: '10', dataType: 'number' },
        name        : { value: 'foo', dataType: 'string' },
        isHoliday   : { value: '1', dataType: 'boolean' },
        // ===== more items here =====
    }
    
    /**
     * Fetch data based on input
     */
    const value = data[key].value // fetch value
    const dataType = data[key].dataType // fetch TypeName
    
    /**
     * Cast data type and return value
     */
    if(dataType === 'number') {
        return Number(value)
    } else if(dataType === 'string') {
        return String(value)
    } else if(dataType === 'boolean') {
        return value === '1'
    }
}


/**
 * Using get() funciton here
 */
const count: number = get('count')
// Error: Type 'string | number | boolean' is not assignable to type 'number'.

const isHoliday: boolean = get('isHoliday')
// Error: Type 'string | number | boolean' is not assignable to type 'boolean'.

我试过TypeScript: function return type based on argument, without overloading
但我未能转换数据类型。

interface TypeRegistry {
    count: number
    name: string
    isHoliday: boolean
    // more items here
}

function get<K extends keyof TypeRegistry>(key: K): TypeRegistry[K] {
    const data: {
        [key: string]: {
            value: string
        }
    } = {
        count       : { value: '10' },
        name        : { value: 'foo' },
        isHoliday   : { value: '1' },
        // more items here
    }
    
    const value = data[key].value // fetch value
    
    // How to cast value to TypeRegistry[K]???
    
    return value
    // Error: Type 'string' is not assignable to type 'TypeRegistry[K]'.
}

/**
 * Using get() funciton here
 */
const count: number = get('count')
const isHoliday: boolean = get('isHoliday')

This answer 使用条件类型,但他们使用key 来决定数据类型。
但在我的情况下,键的数量可能会有所不同。
我希望只使用 TypeNames 而不是键的东西。

但是这种方法也没有成功
代码:


type TypeName = 'number' | 'string' | 'boolean'
type ItemKey = 'count' | 'name' | 'isHoliday'

type ReturnDataType<T> =
    T extends 'count' ? number :
    T extends 'name' ? string :
    T extends 'isHoliday' ? boolean:
    // more items here
    never;


function get<T extends ItemKey>(key: ItemKey): ReturnDataType<T> {
    
    /**
     * Predefined data
     */
    const data: {
        [key: string]: {
            value: string
            dataType: TypeName
        }
    } = {
        count       : { value: '10', dataType: 'number' },
        name        : { value: 'foo', dataType: 'string' },
        isHoliday   : { value: '1', dataType: 'boolean' },
        // more items here
    }
    
    /**
     * Fetch data based on input
     */
    const value = data[key].value // fetch value
    const dataType = data[key].dataType // fetch TypeName
    
    /**
     * Cast data type and return value
     */
    if(dataType === 'number') {
        return Number(value) as ReturnDataType<T>
    } else if(dataType === 'string') {
        return String(value) as ReturnDataType<T>
    } else if(dataType === 'boolean') {
        return Boolean(value) as ReturnDataType<T>
    }
}

/**
 * Using get() funciton here
 */
const count: number = get('count')
// Error:
// Type 'string | number | boolean' is not assignable to type 'number'.
//   Type 'string' is not assignable to type 'number'.ts(2322)

const isHoliday: boolean = get('isHoliday')
// Error:
// Type 'string | number | boolean' is not assignable to type 'boolean'.
//   Type 'string' is not assignable to type 'boolean'.ts(2322)




编辑:从 jcalz answer,我想出了以下代码,它可以正常工作



/**
 * Data type map
 * Usefull only at compile time
 */
type DataTypeMap = {
    'number': number
    'string': string
    'boolean': boolean
}

/**
 * For typescript type hint (compile time)
 */
type TsTypeRegistry = {
    count       : number
    name        : string
    isHoliday   : boolean
    // ===== more items here =====
}

/**
 * Type required at runtime
 */
const JsTypeRegistry: {
    [key: string]: keyof DataTypeMap
} = {
    count       : 'number',
    name        : 'string',
    isHoliday   : 'boolean',
    // ===== more items here =====
}

/**
 * Convert datatype at runtime
 */
function coerce(value: string, type: keyof DataTypeMap) {
    switch(type) {
        case 'number': return Number(value)
        case 'string': return value
        case 'boolean': return value === '1'
        default: throw new Error(`coerce not implemented for type: '${type}'`)
    }
}

/**
 * The get function
 */
function get<K extends keyof TsTypeRegistry>(key: K): TsTypeRegistry[K] {
    const data = {
        count: { value: '10' },
        name: { value: 'foo' },
        isHoliday: { value: '1' },
        // ===== more items here =====
    }
    
    const value = data[key].value
    const dataType = JsTypeRegistry[key] // datatype required at runtime
    
    return coerce(value, dataType) as TsTypeRegistry[K]
    /**
     * Here 'coerce' converts value at runtime
     * and 'TsTypeRegistry[K]' gives correct type at compile time
     */
}

/**
 * Using get function
 */
const count: number = get('count')
console.log(count, typeof count) // 10 'number'

const someName: string = get('name')
console.log(someName, typeof someName) // foo 'string'

const isHoliday: boolean = get('isHoliday')
console.log(isHoliday, typeof isHoliday) // true 'boolean

/**
 * Now the get function converts data type at runtime as well as
 * gives correct type hint at compile time.
 */

【问题讨论】:

    标签: typescript casting return-type


    【解决方案1】:

    看起来您的各种代码示例正在执行您想要的部分,但您并没有将它们捆绑在一个单一的功能版本中。您需要在运行时强制类型(如在第一个示例中)在编译时断言类型(如在第二个示例中),或者您可以重构为编译器可以的版本实际验证类型。


    TypeScript 和 JavaScript 都没有真正按照您的想法进行“转换”。在 JavaScript 中,有时可以将 coerce 值从一种类型转换为另一种类型,例如获取像 "123" 这样的字符串并通过在需要像 +"123" 这样的数字的操作中使用它来将其强制为一个数字。在 TypeScript 中,assert 可以在运行时指定一个值的特定类型,但这只是为编译器提供更多信息,并没有运行时影响。类型强制和类型断言在某些方面都可以被认为是“强制转换”,但它们有足够的不同,因此最好避免歧义并且不要使用“强制转换”这个术语。

    意识到类型强制和类型断言完全不相关是非常重要的。您添加到 TypeScript 的任何类型信息都不会在运行时产生任何影响;注释或断言类型只会影响您看到的编译器警告类型。当 TypeScript 代码编译成 JavaScript 时,TypeScript 的整个静态类型系统是erased。如果您希望某个值在运行时属于特定类型,则需要编写一些代码来实现这一点。

    如果编译器无法确定您所做的是类型安全的,例如在您的第一个示例中,string | number | boolean 不被视为可分配给TypeRegistry[K],您可以使用类型断言或类似的东西来抑制编译器错误。对于像您这样的情况,一个函数有多个 return 行并且没有一个类型检查,我通常将函数编写为带有单个调用签名的 overloaded function。调用签名的类型足够强,允许调用者针对不同的输入类型获得不同的输出类型,而实现签名的类型足够松散,允许string | number | boolean 返回值:

    interface TypeRegistry {
      count: number
      name: string
      isHoliday: boolean
      // more items here
    }
    
    // call signature
    function get<K extends keyof TypeRegistry>(key: K): TypeRegistry[K];
    
    // implementation
    function get(key: keyof TypeRegistry): TypeRegistry[keyof TypeRegistry] {
      const data = {
        count: { value: '10', dataType: 'number' },
        name: { value: 'foo', dataType: 'string' },
        isHoliday: { value: '1', dataType: 'boolean' },
        // ===== more items here =====
      }
    
      const value = data[key].value // fetch value
      const dataType = data[key].dataType // fetch TypeName
    
      if (dataType === 'number') {
        return Number(value);
      } else if (dataType === 'string') {
        return String(value);
      } else if (dataType === 'boolean') {
        return value === '1';
      }
      throw new Error()
    }
    

    您可以验证这是否按需要工作。请注意,重载或类型断言需要您小心;编译器无法验证实现的类型安全性,我们也没有通过使用重载来改变这一点。我们所做的只是告诉编译器不要担心它。如果我们犯了错误(比如切换dataType === 'number' 检查和dataType === 'string' 检查),编译器不会注意到,您会在运行时看到问题。


    如果您希望编译器在不需要断言或重载的情况下实际验证类型安全,您可以重构为如下内容:

    const coerce = (v: string) => ({
      get count() {
        return +v
      },
      get name() {
        return v
      },
      get isHoliday() {
        return v === "1"
      }
    })
    type TypeRegistry = ReturnType<typeof coerce>;
    
    function get<K extends keyof TypeRegistry>(key: K): TypeRegistry[K] {
      const data = {
        count: { value: '10' },
        name: { value: 'foo' },
        isHoliday: { value: '1' },
        // more items here
      }
      return coerce(data[key].value)[key];
    }
    
    /**
     * Using get() funciton here
     */
    const count: number = get('count');
    console.log(typeof count); // "number"
    const isHoliday: boolean = get('isHoliday')
    console.log(typeof isHoliday) // "boolean"
    

    我们仍然在运行时强制这些值,但现在编译器可以跟随我们正在做的事情。这是因为coerce() 函数的输出被视为TypeRegistry 类型的值...它恰好是用getters 实现的,因此实际上只调用了一个强制代码路径。并且编译器确实知道使用 K 类型的键索引到 TypeRegistry 类型的值将产生 TypeRegistry[K] 类型的值。

    Playground link to code

    【讨论】:

    • 我所说的投射,是指运行时。正在转换的实际值。
    • 感谢您的详细解释 :) 我已经更新了我的问题中的工作代码。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-03-20
    • 2016-07-17
    • 2018-08-12
    • 1970-01-01
    • 2020-09-01
    • 2020-02-02
    • 2016-08-31
    相关资源
    最近更新 更多