根据特定键的预定义数据类型转换字符串值的 Typescript 函数

Typescript function which casts string value based on predefined datatype for particular keys

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


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

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

在 returning 值转换为指定数据类型之前。

例如:

get 函数 return 具有正确类型的正确值。
但是 return typescript 在 funciton called 地方没有正确推断出类型。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'.

我试过了
但是我没有转换数据类型。

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')

使用条件类型,但他们使用 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 ,我想出了以下代码并且它工作正常



/**
 * 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 和 JavaScript 都没有真正按照您的想法进行“转换”。在 JavaScript 中,有时可能 coerce a value from one type to another, such as taking a string like "123" and coercing it to a number by using it in an operation that expects a number like +"123". And in TypeScript it is possible to assert 值在运行时是特定类型,但这只是向编译器提供更多信息,并没有运行时效果。类型强制和类型断言在某些方面都可以被认为是“强制转换”,但它们有很大的不同,因此最好避免歧义,不要使用术语“强制转换”。

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

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

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