2

I want to write a small language by TypeScript and I define the basic data type for it:

type BasicDataType = {
    kind: 'text'
} | {
    kind: 'number'
};

Then I defined a generic type to express its instance:

type BasicInstance<B extends BasicDataType> = B extends { kind: 'number' } ?
    number
    : B extends { kind: 'text' } ?
    string
    : never;

let a: BasicInstance<{ kind: 'number' }> = 1;
let b: BasicInstance<{ kind: 'text' }> = '';

It works well, but when I try to define a advanced type and its instance:

type DataType = {
    kind: 'single',
    t: BasicDataType
} | {
    kind: 'array',
    t: BasicDataType,
};
type Instance<D extends DataType> = D extends { kind: 'single', t: infer B } ?
    BasicInstance<B>
    : D extends { kind: 'array', t: infer B } ?
    Array<BasicInstance<B>>
    : never;

I got the error:

error TS2344: Type 'B' does not satisfy the constraint 'BasicDataType'. Type 'B' is not assignable to type '{ kind: "number"; }'.

It seems TypeScript cannot understand that B must be a BasicDataType. Why it happen? And How do I fix it?

5
  • Why don't you just use BasicDataType instead of B? tsplay.dev/Nd34Yw Commented Mar 6, 2021 at 18:06
  • I think B is one kind of BasicDataType (maybe {kind: 'text'} or {kind: 'number'}}), it depends on what D is. Commented Mar 6, 2021 at 18:11
  • For both cases in DataType, t will just be BasicDataType. The type engine cannot know whether it's { kind: 'text' } or { kind: 'number' }. The only thing it could be derive is B = BasicDataType. Commented Mar 6, 2021 at 18:27
  • OK, but the error message seems typescript could not derive B = BasicDataType? Commented Mar 6, 2021 at 18:44
  • That's indeed the error. I believe this is just a limitation of TypeScript, in that it doesn't take into account any constraints on the inferred types, but I haven't found a clear description of it yet. Commented Mar 6, 2021 at 18:54

1 Answer 1

2

The inferred types are not always as exact as you would expect. Take this type definition for example:

type NotWorking<T extends {x: {y: number}}> =
  T extends {x: infer N} ? N['y'] : never

It fails with a Type '"y"' cannot be used to index type 'N'., even though because of the T extends {x: {y: number}} constraint, N['y'] should exist. To make it type check, you can add another condition N extends {y: number}, which will always pass:

type Working<T extends {x: {y: number}}> =
  T extends {x: infer N} ? N extends {y: number} ? N['y'] : never : never

For your type you could put an B extends BasicDataType extra condition on the outside and use infer K for the kind so you only need one extends condition to cover all kinds:

type Instance<D extends DataType> =
  D extends { kind: infer K, t: infer B }
  ? B extends BasicDataType
    ? K extends 'single' ? BasicInstance<B> 
    : K extends 'array' ? Array<BasicInstance<B>>
    : never
   : never
: never

TypeScript playground

Sign up to request clarification or add additional context in comments.

3 Comments

This is a good answer. I don’t like to sidestep the actual questions that people are asking, but I will say that in this example a union type makes more sense than a conditional.
Fair point :-) I tend to try and answer the question as asked though as often it will be a stripped down version of the actual problem anyway.
I'm reading the question again and I kind of missed the "two layer" part at first so it's not as straightforward of a case for union types as I was thinking it was anyways.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.