EDIT: I have updated working code at bottom of the question.
I'm having a function called get
which accepts a key
as parameter and returns corresponding
value.
This function maintains value
(which is always string) and datatype
for each key
.
Like:
key value(stored as string) dataType
-------------------------------------------------
count '10' number
name 'foo' string
isHoliday '1' boolean
===== more rows here =====
Before returning value it casts to specified datatype.
For ex:
count
, its value '10'
is casted to datatype number
and value is 10
isHoliday
, its value '1'
is casted to datatype boolean
and value is true
The get
function returns correct value with correct type.
But return type is not correctly inferred by typescript at funciton called place.const count: number = get('count')
And produces type error like: Type 'string | number | boolean' is not assignable to type 'number'.
Code:
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'.
I tried TypeScript: function return type based on argument, without overloading.
But I failed to cast datatype.
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 uses conditional types but they are using key
to decide datatype.
But in my case number of keys might vary.
I wish something which works using just TypeNames instead of keys.
But no success with this approach also
Code:
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)
EDIT: From jcalz answer, I came up with following code and it works correctly
/**
* 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.
*/
It looks like your various code examples are doing pieces of what you want, but you have not tied it together in a single functioning version. You need to both coerce the types at runtime (as in your first example) and either assert the types at compile time (as in your second example), or you could refactor to a version that the compiler can actually verify the types.
Neither TypeScript nor JavaScript really has "casting" the way you're thinking about it. In JavaScript it is sometimes possible to 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 that a value will be of a specific type at runtime, but this is just giving more information to the compiler, and has no runtime effect. Both type coercion and type assertions can be thought of as "casting" in some ways, but they are different enough that it's best to avoid ambiguity and not use the term "casting".
It is very important to realize that type coercion and type assertions are not related to each other at all. No type information that you add to TypeScript will have any effect at runtime; annotating or asserting types only affect what sorts of compiler warnings you see. The entire static type system of TypeScript is erased when TypeScript code is compiled to JavaScript. If you want a value to be of a particular type at runtime, you will need to write some code to make that happen.
If the compiler cannot figure out that what you are doing is type safe, such as in your first example where string | number | boolean
is not seen as assignable to TypeRegistry[K]
, you can use a type assertion or something similar to suppress the compiler error. For a situation like yours where a function has multiple return
lines and none of them type check, I usually write the function as an overloaded function with a single call signature. The call signature is strongly typed enough to allow callers to get different output types for different input types, while the implementation signature is loosely typed enough to allow string | number | boolean
return values:
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()
}
You can verify that this works as desired. Note that an overload or a type assertion require care on your part; the compiler cannot verify the type safety of the implementation, and we have not changed this by using an overload. All we've done is told the compiler not to worry about it. If we make a mistake (say by switching around the dataType === 'number'
check and the dataType === 'string'
check), the compiler won't notice and you'll see problems at runtime.
If you want the compiler to actually verify type safety without requiring assertions or overloads, you can refactor to something like this:
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"
We are still coercing the values at runtime, but now the compiler can follow what we're doing. This works because the output of the coerce()
function is seen as a value of type TypeRegistry
... it happens to be implemented with getters, so that only one coercion code path is actually called. And the compiler does understand that indexing into a value of type TypeRegistry
with a key of type K
will produce a value of type TypeRegistry[K]
.