Sorry about the question title, I am not quite sure how to phrase it.
I am fairly new to Typescript and wanted to add strong types to a new project.
Lets say I have a function that accepts two parameters: value and params written like
type Value = 'category' | 'follows' | 'users';
request(value: Value, params: object)
As you can see Value type only accepts three predefined values. But params should be conditionnaly typed when value changes:
If value is category, then params should type as { author: string, year: number }, if it is users then it should type { id: number } OR { name: string }
And that's the whole challenge of it. I can do it with objects and indexes but have been stuck with types.
Is it even possible to do this ?
Thanks a lot !
NVH
CodePudding user response:
There's a lot of ways to do this, but here are two.
Function overloads (docs)
function request(value: 'category', params: { author: string, year: number }): void
function request(value: 'users', params: { id: number } | { name: string }): void
function request(
value: 'category' | 'users',
params: { author: string, year: number } | { id: number } | { name: string }
) {
// TODO: implementation
}
Lookup type with a generic function (docs)
type Value = 'category' | 'follows' | 'users';
type ObjectLookup = {
category: { author: string, year: number }
users: { id: number } | { name: string }
follows: { foo: string }
}
function request<T extends Value>(value: T, params: ObjectLookup[T]) {
// TODO: implementation...
}
CodePudding user response:
It works only with TS nightly, see this PR.
type Value = 'category' | 'follows' | 'users';
type Arguments =
| ['category', { author: string, year: number },]
| ['users', { id: number } | { name: string }]
| ['follows', Record<PropertyKey, unknown>]
const request = (...[value, params]: Arguments) => {
if (value === 'category') {
params.author // ok
params.year
}
return 'UNIMPLEMENTED'
}
See rest-parameters-with-tuple-types for more explanation about using spread syntax in ts
However, you might have a problems with users. See example:
const request = (...[value, params]: Arguments) => {
if (value === 'category') {
params.author // ok
params.year
}
if(value==='users'){
params.id // <------------- error
}
return 'UNIMPLEMENTED'
}
TypeScript does not allow you to get params.id. In fact, TS does not allow you to get any property of params if value is users because union { id: number } | { name: string } does not have common properties. Hence, the safest way is to forbid using any property.
In order to narrow params type when value is users I think it worth using custom type guard:
type Value = 'category' | 'follows' | 'users';
type UsersId = { id: number }
type UsersName = { name: string }
// TYPE GUARD
const isUserId = (data: UsersId | UsersName): data is UsersId => 'id' in data
// TYPE GUARD
const isUserName = (data: UsersId | UsersName): data is UsersName => 'name' in data
type Arguments =
| ['category', { author: string, year: number },]
| ['users', UsersId | UsersName]
| ['follows', Record<PropertyKey, unknown>]
const request = (...[value, params]: Arguments) => {
if (value === 'category') {
params.author // ok
params.year
}
if (value === 'users') {
if(isUserId(params)){
params.id // ok
}
}
return 'UNIMPLEMENTED'
}
CodePudding user response:
Example:
type Value = 'category' | 'follows' | 'users';
function request(value: 'category', params: Type1):ResponseType;
function request(value: 'follows', params: Type2):ResponseType;
function request(value: 'users', params: Type3):ResponseType;
function request(value: Value, params: Object):ResponseType{
...implementation
}
TS is able to see what the type of a prop is and infer what the other props in the respective variant of an overloaded function are.
