Skip to content

Infer union type based on string literalΒ #51734

@kerwanp

Description

@kerwanp

Suggestion

πŸ” Search Terms

  • typescript infer union type

βœ… Viability Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

Adding a way to infer the correct type in a union using a value of its own key.

πŸ“ƒ Motivating Example

If I have the following type, it is a simple Union where we always have a type key and we find a key of the value of the type.

You can see it directly in the playground:
https://www.typescriptlang.org/play?#code/C4TwDgpgBAKgjFAvFA3gKClUkBcUDkwAlsADYT4A0GWJ5eAzsAE5EB2A5mgL5QA+qGtgh58AYwAWEMQGsARgHsAHlRqTp85XkULyAQzY80aMQrZMoYZgsjNiEBnngBtALpIozmukyZho4jIKal9aINEACQhSUgUoAHcFZlIAE1VMbhDBUP8CdVlFFSzMfM0lPAAzPVIGCBpuNFdjKxsIOyIHADoAWz0wAApopAA+bKgAenGoACEQKFL2DiwpKAA3aoBXaAUK5ehhKBkIEEo9qHYKtog2MWgiBigUszrMIl3B0k6DxB+CQPJ8ABKKDMCDADbMNhQaLOaJfcAQJo0SawKSg84PMykObPSwKBgMIhyIikEhzdhQBQQqAbNhEMw0UHgyHQ0iwz7CJrcQHNay2ewMHp9D4jMYogCSbEuoJudwebAUwEeuIMKWW92VDigCqViWYMmhq2u5121VIWARDwpVOYNLpZigvWAkkZYIhUJhcM5PEBQA

type T1 = {
  type: 'title',
  title: string
} | {
  type: 'checkbox',
  checkbox: boolean
}

const properties: T1[] = [
  {
    type: 'title',
    title: 'Hello world',
  },
  {
    type: 'checkbox',
    checkbox: false
  }
]

properties.map(el => {
  // By checking the value of the type key, the inference is done
  if (el.type === 'title') return el[el.type]

  // There is only one possibility in our union
  return el[el.type]
})

properties.map(el => {
  // Inference is not done and this does not work even if all types in our union match
  return el[el.type]
})

πŸ’» Use Cases

I am querying an external API that returns me a list of properties:

{
    "properties": {
        "isActive": {
            "type": "checkbox",
            "checkbox": true
        },
        "email": {
            "type": "email",
            "email": "hello@world.com"
        },
        "content": {
            "type": "rich_text",
            "rich_text": {
                "plain_text": "Hello world",
                "blocks": []
            }
        }
    }
}

I want to normalize this data into:

{
    "isActive": true,
    "email": "hello@world.com",
    "content": "Hello world"
}

There are some specific property types like rich_text where I need a specific normalization process.
And by default I simply want to return the value.

type PropertyTypes = 'email' | 'checkbox' | 'rich_text'

type PropertyDenormalizedValueMap = {
  'email': string;
  'checkbox': boolean;
  'rich_text': {
    plain_text: string,
    blocks: any[]
  }
};

type PropertyNormalizedValueMap = {
  'email': string;
  'checkbox': boolean;
  'rich_text': string;
};

type PropertyObject<T extends PropertyTypes> = {
  [key in T as string]: {
    type: key;
  } & {
    [property in key]: PropertyDenormalizedValueMap[key];
  };
}[T];

interface Normalizer<T extends PropertyTypes = PropertyTypes> {
  normalize(data: PropertyObject<T>): PropertyNormalizedValueMap[T];
}

class RawNormalizer implements Normalizer<'email'|'checkbox'> {
  normalize(data: PropertyObject<'email'|'checkbox'>): PropertyNormalizedValueMap['email'|'checkbox'] {
    // By checking the `type` key it infers the Type
    if (data.type === 'checkbox') return data[data.type];

    // The only possible type in our union make this possible
    return data[data.type]
  }

  normalize2(data: PropertyObject<'email'|'checkbox'>): PropertyNormalizedValueMap['email'|'checkbox'] {
    // This is not possible as there is no inference done
    return data[data.type]
  }
}

class RichTextNormalizer implements Normalizer<'rich_text'> {
  normalize(data: PropertyObject<'rich_text'>): PropertyNormalizedValueMap['rich_text'] {
    return data.rich_text.plain_text
  }
}

Here is a playground to test the above code: https://www.typescriptlang.org/play?#code/C4TwDgpgBACgTge0nUAVcEDOUC8UDkEAtgIYCWANvlAD4EDGAFhPQNYBGCAHtXfnGSYB9YBC7B8AKEmhIsRMlAARCADsEcUhTIAvCABMAaiQoBXCAFkSYXFADekqAWLkqALiiZgA1QHMA3I4MzGycPB6cCBQQJKqBTvyCjCJiEh4OTk5gFOSqKeIeXj6+ADRBTuwUCGyYHrEgANoAukEAvpKtgTIY8kgQKCAAchpaugbGZpbWthnOpJT4hd5kfvHBLBzci1CR0bFricKi4ttFKwEdXbLQ8H0DAPLsAFYswAA8qFCpavrYt4ogdCQTAAPhmQQarAgICgKygnxI2DOfia6XKUGuHihIDWrSgADJ7OiGmAFP1QLDVFBsajegCVOpNCYxkYTOYrGBIdCmrjAq0GqgedIVqI4AAzEj0aDDJnaPRwD5fcQ-P5kgZArC2f7kwEYUFEpyM0Z6AAU+hIwBIHm1D2erw+IIAlNa1aAZcbxmyppzBXzpPQcphsAAlEgAd3dzPlsKI2WIamA2Ejcv6b0I8yoNHwTA2YXwYNmRqjEDNFqtdJ1jxe9He6dc+CzOdCWydLrubpGxdZkw5DTrC0bIU2PCaBsyAHpx1AAEIwpusc4Y5hQAAG1xX1OhsOAlLF-WwwGXGvRZDFUFLloAdNdcDg8Nmh3nHVA4BBgKY4FTzZaGt+SNeMCFdFJ3hZcEFUCgYTABAgzISpoBvOEEA-KBTFUMhwKgUgoSXMhsGg2D4PRV930-KA-1-MsAMgFonHaIIixTAAmC9yxtUAq3tftMwfXMW2dCsBmTFkJnZaw+xcAdeObEcxycEDUEYPDYWwdQdwIzA4OiKBESXfpoGU9Rd301QpXI8CIGIt8Py-MtKKva5aKgdp6IDREQySVBUmE6MyFjaIiATJNOxTBVDmSY4JALBiQrGVi2wBTiazTAQjlSfMBPYoZYr0bsxM5cL8gkUdZicEibPIqjUoi1JL2yXIiraDogA

Metadata

Metadata

Assignees

No one assigned

    Labels

    DuplicateAn existing issue was already created

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions