Search code examples
javascripttypescripttemplate-literals

Best way to declare string structure with TypesScript?


Let say I have this JavaScript object:

const myObject = {
   id: 'my_id', // Base value that following value's structures will be depended on (always pascal case). 
   upperID: 'MY_ID', // Uppercase of `id`.
   camelID: 'myId', // Camel case of `id`.
}

I want to use TypeScript to ensure that id is always pascal lower case. The upperID and camelID has same "value" with different string structure as above. What would be the best way to declare this myObject type in TypeScript?


Solution

  • The upper case one is pretty easy with the provided Uppercase<T> utility type.

    type CasedIds<ID extends string> = {
        id: ID,
        upperID: Uppercase<ID>
    }
    
    
    const myObject: CasedIds<'my_id'> = {
       id: 'my_id',
       upperID: 'MY_ID',
    } as const
    

    See playground


    The camel case one gets tricky. So first you need a type that can do this to a string.

    There's probably a few ways to do this. Here's one.

    type CamelCase<T extends string> =
        T extends `${infer Pre}_${infer Letter}${infer Post}`
            ? `${Pre}${Capitalize<Letter>}${CamelCase<Post>}`
            : T
    
    type TestCamel = CamelCase<'abc_def_ghi'> // 'abcDefGhi'
    

    See playground

    Let's walk through this.

    This generic type takes a string type as the generic parameter T. It then check to see if T extends a string of a certain pattern that contains an underscore followed by some characters.

    If it does, then infer some substrings from that pattern. Infer the part before the underscore as Pre, the character after the underscore as Letter and the rest of the string as Post. Else, just use the string type as is.

    Then we can make a new string from the Pre the Letter capitalized, and the Post. But there maybe more underscores, so we do the whole thing again on Post. This is a recursive type that stops recursing when there are no underscores left.


    Using that the rest is easy:

    type CamelCase<T extends string> =
        T extends `${infer Pre}_${infer Letter}${infer Post}`
            ? `${Pre}${Capitalize<Letter>}${CamelCase<Post>}`
            : T
    
    type CasedIds<ID extends string> = {
        id: ID,
        upperID: Uppercase<ID>
        camelID: CamelCase<ID>
    }
    
    
    const myObject: CasedIds<'my_id'> = {
       id: 'my_id',
       upperID: 'MY_ID',
       camelID: 'myId',
    } as const
    

    See playground


    Although you'll probably want a function build these for you:

    function makeId<T extends string>(id: T): CasedIds<T> {
        return {} as unknown as CasedIds<T> // mock a real implementation
    }
    
    const myId = makeId('my_id')
    myId.id       // type: 'my_id'
    myId.upperID  // type: 'MY_ID'
    myId.camelID  // type: 'myId'
    

    See playground