Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Conditional object types when value changes

Tags:

typescript

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 conditionally 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

EDIT: Thanks a lot for all your help, I had a good time reading all of the documentation as well, since I want to have the safest types for my function I will go for @captain-yossarian solution but the others worked as well!

like image 228
nvh Avatar asked Sep 15 '26 14:09

nvh


2 Answers

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

Playground

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'
}
like image 119
captain-yossarian Avatar answered Sep 18 '26 04:09

captain-yossarian


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
}

Playground


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...
}

Playground

like image 26
Alex Wayne Avatar answered Sep 18 '26 05:09

Alex Wayne



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!